docs: apply 27 corrections from the adversarial triage, and rework the perf lane

The triage over the 43 unaddressed verdicts returned 4 code and 35 doc items.
The code ones shipped in e24b46d; these are the documents.

Twenty-one applied mechanically after a validation pass confirmed each locator
still matched exactly once and each replacement was supported by the tree. Ten
were held back because a re-verify pass found them mutually exclusive -- three
patches rewriting the same benchmarks/README block, three rewriting the same
OCS paragraph -- and applying them in sequence would have corrupted the files.
Those were resolved by hand into one coherent edit each. One was rejected.

The corrections worth naming:

- **The A/B table contradicted itself in three files.** benchmarks/README.md
  and CLAUDE.md said 24 checks / 21-24 / 87.5% / "fixed 3" while the suite has
  26 and the published JSON says 21/26, 80.8%, five fixed. All three now agree,
  and the five are split into the three new-capability `miss -> pass` and the
  two repaired-defect `fail -> pass`, which is the distinction the lane exists
  to make.
- **"Every number below is produced by a script"** was false twice over:
  section 5's nine scores are literals a human assigned, and section 4 now
  quotes hand-collected timings. The claim is narrowed to the charts, which
  really are all rendered from committed data, and both exceptions are named.
- **The OCS paragraph over-quantified.** TEXT rotation was given as the single
  exception; z components and HATCH boundaries are also entity-frame, and the
  live backend records `2d_polyline_coordinates_not_verified_against_live_autocad`
  in the same reason string. It now points at `ocs_normalized` as the live list.
- **Transactions were shown as bare parity.** The server itself grades them
  differently -- `FeatureCapability(True, "undo_mark")` on COM against
  `(True, "snapshot")` headless -- and COM rollback sends `_UNDO B` without
  confirming it landed. The row says so.
- **"Raw commands: Opt-in"** named a switch that does not exist. Both tools are
  registered unconditionally; `DANGEROUS_COMMANDS_ENABLED` only disables the
  sanitizer.
- **"Wave A shipped 13 of a planned 39"** hardened an approximation: the only
  source writes `~39`.
- **CAD_PROGID** said nothing beyond the connection was tested against
  BricsCAD/ZWCAD/GstarCAD, implying the connection was. It was not.

Perf lane reworked against a fresh paired measurement. The old text claimed
"28% slower than v1.4.0, median of three runs" and that
`EZDXF_CALL_TIMEOUT=0` restored v1.4.0's numbers "exactly". Three problems:
`perf_suite` runs each workload once per invocation and cannot produce a
median, "exactly" was refuted by the figures in its own sentence, and the
number moved when the timeout wrapper was rebuilt. It now reports the guard's
cost -- +20.8% and +19.9%, three paired runs, median taken by hand -- says the
figures have no published artifact, and says the wrapper rework recovered about
a third of the v1.5.0 overhead rather than closing it.

perf-ezdxf.json and the perf chart regenerated against the shipped code, and
the README table rebuilt from that artifact so the two cannot drift.

1267 passed, 1 xfailed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Umut CAN
2026-08-06 15:55:48 +03:00
co-authored by Claude Opus 5
parent e24b46d6f0
commit 6b9860e68e
5 changed files with 415 additions and 373 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ Tools are organized into sections (counts are indicative — `system_about` is a
**Measuring:** never read vertices back and shoelace them — that loses 28.2% of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry, and its payload states its own accuracy: `exact`, `flatten_tolerance`, `assumed_closed`, `self_intersecting`. A HATCH reports the area it *fills* (outer loops minus islands) plus `hatch_style`; on a curved hatch edge `flatten_tolerance` is not the accuracy knob, because ezdxf hands boundaries over as cubic Beziers whose ~0.028% circle error is already baked in. `boundary_trace` / `boundary_from_entities` report the same number `analysis_measure_entity` gives for the polyline they just drew — if those ever diverge, one of them is approximating and neither says so. **Measuring:** never read vertices back and shoelace them — that loses 28.2% of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry, and its payload states its own accuracy: `exact`, `flatten_tolerance`, `assumed_closed`, `self_intersecting`. A HATCH reports the area it *fills* (outer loops minus islands) plus `hatch_style`; on a curved hatch edge `flatten_tolerance` is not the accuracy knob, because ezdxf hands boundaries over as cubic Beziers whose ~0.028% circle error is already baked in. `boundary_trace` / `boundary_from_entities` report the same number `analysis_measure_entity` gives for the polyline they just drew — if those ever diverge, one of them is approximating and neither says so.
**Benchmark matrix:** `benchmarks/tasks_v3.py` is the current set (15 tasks; `--matrix v2` reproduces the v1.4 ten). The published competitor reports are pinned v1.4 runs against v2 and are **never** back-filled for the five v3 tasks — the chart shows them `not run`, not zero. `benchmarks/correctness_suite.py` (24 checks) is the release A/B gate: `python benchmarks/compare_versions.py v1.4.0`. **Benchmark matrix:** `benchmarks/tasks_v3.py` is the current set (15 tasks; `--matrix v2` reproduces the v1.4 ten). The published competitor reports are pinned v1.4 runs against v2 and are **never** back-filled for the five v3 tasks — the chart shows them `not run`, not zero. `benchmarks/correctness_suite.py` (26 checks) is the release A/B gate: `python benchmarks/compare_versions.py v1.4.0`.
### Adding a New Tool ### Adding a New Tool
+49 -23
View File
@@ -69,7 +69,7 @@ AUTOCAD_MCP_BACKEND=com autocad-mcp # live AutoCAD (pip install "autocad-mc
| *(none)* | `fastmcp`, `ezdxf`, `pydantic` | Headless DXF on any OS — **no rendering** | | *(none)* | `fastmcp`, `ezdxf`, `pydantic` | Headless DXF on any OS — **no rendering** |
| `[com]` | `pywin32`, `Pillow` | Live AutoCAD control + window capture | | `[com]` | `pywin32`, `Pillow` | Live AutoCAD control + window capture |
| `[pdf]` | `matplotlib` | PDF export and headless PNG rendering, any platform | | `[pdf]` | `matplotlib` | PDF export and headless PNG rendering, any platform |
| `[full]` | everything above | Development and CI | | `[full]` | everything above | Live AutoCAD and rendering in one install |
> [!NOTE] > [!NOTE]
> The bare install **cannot draw pixels**. `ezdxf.addons.drawing.frontend` does an unconditional top-level `import PIL.Image`, so Pillow is required by *every* ezdxf render path — PNG screenshot and PDF export alike — not just by the COM window capture it is listed under. Add `[pdf]` (or `[full]`) if you want images on Linux or macOS. > The bare install **cannot draw pixels**. `ezdxf.addons.drawing.frontend` does an unconditional top-level `import PIL.Image`, so Pillow is required by *every* ezdxf render path — PNG screenshot and PDF export alike — not just by the COM window capture it is listed under. Add `[pdf]` (or `[full]`) if you want images on Linux or macOS.
@@ -106,13 +106,13 @@ Works with Claude Desktop, Cursor, or any stdio MCP host. For HTTP: `autocad-mcp
<tr><td><b>Engineering generators</b></td><td>involute gears (front + section A-A), DIN 6885 keyed bores, ISO A3 title block</td></tr> <tr><td><b>Engineering generators</b></td><td>involute gears (front + section A-A), DIN 6885 keyed bores, ISO A3 title block</td></tr>
<tr><td><b>Discovery</b> ✨</td><td><code>search_tools</code> ranked over an AutoCAD command and synonym corpus — <code>FILLET</code>, <code>BPOLY</code>, <code>QSELECT</code>, <code>WBLOCK</code>, <code>OVERKILL</code>, <code>CHSPACE</code> each rank <b>#1</b> of the 149-tool advertised catalog</td></tr> <tr><td><b>Discovery</b> ✨</td><td><code>search_tools</code> ranked over an AutoCAD command and synonym corpus — <code>FILLET</code>, <code>BPOLY</code>, <code>QSELECT</code>, <code>WBLOCK</code>, <code>OVERKILL</code>, <code>CHSPACE</code> each rank <b>#1</b> of the 149-tool advertised catalog</td></tr>
<tr><td><b>Batching</b> ✨</td><td><code>cad_batch</code> runs a whole step list in one round trip; <code>fields=</code> projects the response on 11 result-heavy tools (a closed, gated list)</td></tr> <tr><td><b>Batching</b> ✨</td><td><code>cad_batch</code> runs a whole step list in one round trip; <code>fields=</code> projects the response on 11 result-heavy tools (a closed, gated list)</td></tr>
<tr><td><b>Paper space</b> ✨</td><td>tab lifecycle, viewports and <code>drawing_export_pdf(layout=...)</code> on both engines; <code>entity_change_space</code> (CHSPACE) headless only</td></tr> <tr><td><b>Paper space</b> ✨</td><td>8 of the 12 layout tools are new — <code>layout_delete/rename/copy</code>, <code>viewport_list/set_scale/lock/delete</code>, <code>entity_change_space</code> (CHSPACE, headless only); layout listing, creation and activation, <code>viewport_create</code> and <code>drawing_export_pdf(layout=...)</code> shipped in v1.4.0, which wrongly declared headless viewport rendering impossible</td></tr>
<tr><td><b>Selection</b> ✨</td><td>window vs crossing stated back to the caller; a polygon tested against its own shape, not its bounding box</td></tr> <tr><td><b>Selection</b> ✨</td><td>window vs crossing stated back to the caller; the polygon is tested as a shape, not as its bounding box — the entity side is still <i>its</i> bounding box, on both engines</td></tr>
<tr><td><b>Boundaries</b> ✨</td><td><code>boundary_trace</code> (BOUNDARY/BPOLY) chains loose edges into one closed polyline, arcs kept as bulges — <i>headless engine only</i></td></tr> <tr><td><b>Boundaries</b> ✨</td><td><code>boundary_trace</code> (BOUNDARY/BPOLY) chains loose edges into one closed polyline; ARC edges keep their exact bulges, while a bulged polyline segment or any other curved edge is chained as its chord — <i>headless engine only</i></td></tr>
<tr><td><b>Measurement</b> ✨</td><td><code>analysis_measure_entity</code> measures what is <i>in</i> the drawing, by handle</td></tr> <tr><td><b>Measurement</b> ✨</td><td><code>analysis_measure_entity</code> measures what is <i>in</i> the drawing, by handle</td></tr>
<tr><td><b>Hatch depth</b> ✨</td><td>gradients, in-place edits and island styles on both; typed edge boundaries headless only</td></tr> <tr><td><b>Hatch depth</b> ✨</td><td>gradients, in-place edits and island styles on both; typed edge boundaries headless only</td></tr>
<tr><td><b>Annotation objects</b> ✨</td><td>WIPEOUT, REVCLOUD, MTEXT background masks — <i>headless only, ActiveX exposes no member</i> — plus text find/replace on both</td></tr> <tr><td><b>Annotation objects</b> ✨</td><td>WIPEOUT, REVCLOUD, MTEXT background masks — <i>headless only, ActiveX exposes no member</i> — plus text find/replace on both</td></tr>
<tr><td><b>3D solids</b></td><td><code>solid_box/cylinder/extrude/revolve/boolean</code> on live AutoCAD (<code>ENABLE_3D=true</code>)</td></tr> <tr><td><b>3D solids</b></td><td><code>solid_box/cylinder/extrude/revolve/boolean</code> on live AutoCAD (<code>ENABLE_3D=true</code>) — <i>unverified against a live seat: they shipped in v1.4.0, and the v1.5.0 live run covered only what that release added</i></td></tr>
<tr><td><b>Quality loop</b></td><td><code>drawing_preflight</code> → <code>drawing_plan</code> → <code>drawing_critique</code> → <code>drawing_refine</code> → <code>drawing_finalize</code> (0–100 score)</td></tr> <tr><td><b>Quality loop</b></td><td><code>drawing_preflight</code> → <code>drawing_plan</code> → <code>drawing_critique</code> → <code>drawing_refine</code> → <code>drawing_finalize</code> (0–100 score)</td></tr>
<tr><td><b>Delivery</b></td><td><code>drawing_deliver</code>: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks</td></tr> <tr><td><b>Delivery</b></td><td><code>drawing_deliver</code>: DXF/PDF/PNG + SHA-256 manifest + reopen-parity checks</td></tr>
</table> </table>
@@ -123,7 +123,9 @@ Alongside the tools, **6 MCP resources** cost nothing in the tool budget and are
### Two rules worth knowing before you draw ### Two rules worth knowing before you draw
**Every coordinate in and out of a tool is WCS, on both engines** — with one deliberate exception. DXF stores CIRCLE / ARC / LWPOLYLINE / TEXT / INSERT geometry in a per-entity frame, and before v1.5.0 the word `extrusion` appeared nowhere in this codebase, so a mirrored or rotated entity reported coordinates that were quietly wrong. An entity in a plane tilted out of WCS XY now reports `plane_normal` and omits the fields xy cannot express, rather than answering with a number that looks fine. The exception is **TEXT `rotation`**, which stays in the entity frame on purpose: a mirrored TEXT is mirror-imaged, and no single scalar angle expresses that. **Coordinates come out in WCS, and the exceptions are declared rather than hidden.** DXF stores CIRCLE / ARC / LWPOLYLINE / TEXT / INSERT geometry in a per-entity frame, and before v1.5.0 the word `extrusion` appeared nowhere in this codebase, so a mirrored or rotated entity reported coordinates that were quietly wrong. An entity in a plane tilted out of WCS XY now reports `plane_normal` and omits the fields xy cannot express, rather than answering with a number that looks fine.
Three things stay in the entity frame on purpose, and `system_capabilities`' `ocs_normalized` reason string is the live list: **TEXT `rotation`** (a mirrored TEXT is mirror-imaged, and no single scalar angle expresses that), **z components**, and **HATCH boundary geometry**. On the live backend that same reason string also records `2d_polyline_coordinates_not_verified_against_live_autocad` — read it at runtime rather than trusting this paragraph.
**Never read vertices back and shoelace them.** That loses **28.2%** of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry and states its own accuracy: `exact`, `flatten_tolerance`, `assumed_closed`, `self_intersecting`. **Never read vertices back and shoelace them.** That loses **28.2%** of the area on a semicircular edge, silently. `analysis_measure_entity(handle)` reads the real geometry and states its own accuracy: `exact`, `flatten_tolerance`, `assumed_closed`, `self_intersecting`.
@@ -137,7 +139,7 @@ There are **27 capability keys**. `system_capabilities` returns the live map, an
|---|:---:|:---:| |---|:---:|:---:|
| Live document control | ✅ | — | | Live document control | ✅ | — |
| Cross-platform, no AutoCAD | — | ✅ | | Cross-platform, no AutoCAD | — | ✅ |
| Transactions and rollback | ✅ | ✅ | | Transactions and rollback | ✅ *undo mark; rollback sends `_UNDO B` and AutoCAD never confirms it landed — best-effort, not atomic* | ✅ *full DXF snapshot, restored synchronously* |
| Paper-space layouts + viewports | ✅ | ✅ | | Paper-space layouts + viewports | ✅ | ✅ |
| Viewport model-content rendering | ✅ | ✅ ᵐ *(no viewport borders)* | | Viewport model-content rendering | ✅ | ✅ ᵐ *(no viewport borders)* |
| Selection window / crossing / polygon | ✅ | ✅ | | Selection window / crossing / polygon | ✅ | ✅ |
@@ -152,18 +154,18 @@ There are **27 capability keys**. `system_capabilities` returns the live map, an
| CHSPACE (`entity_change_space`) | — *unverified against a live seat* | ✅ ᶜ | | CHSPACE (`entity_change_space`) | — *unverified against a live seat* | ✅ ᶜ |
| Undo history | ✅ | opt-in — `EZDXF_UNDO_DEPTH` defaults to `0` | | Undo history | ✅ | opt-in — `EZDXF_UNDO_DEPTH` defaults to `0` |
| TABLE and MLEADER | Native | Portable composite | | TABLE and MLEADER | Native | Portable composite |
| Raw commands / AutoLISP | Opt-in | — | | Raw commands / AutoLISP | ✅ *on by default, denylist-sanitized — `DANGEROUS_COMMANDS_ENABLED=true` removes the denylist* | — *no live command line* |
| Screenshots and PDF export | AutoCAD window capture | Matplotlib render ᵐ | | Screenshots and PDF export | AutoCAD window capture | Matplotlib render ᵐ |
<sub>ᵐ Needs matplotlib — the `[pdf]` or `[full]` extra. Without it `png`, `pdf`, `viewport_render` and `handle_overlay` all report unsupported on the headless engine.<br> <sub>ᵐ Needs matplotlib — the `[pdf]` or `[full]` extra. Without it `png`, `pdf`, `viewport_render` and `handle_overlay` all report unsupported on the headless engine.<br>
ᶜ The headless CHSPACE carries four named restrictions in its own capability reason: top-view untwisted viewports only, dimensions refused unless frozen, ACIS/proxy/table refused, and viewport clipping reported rather than applied.</sub> ᶜ The headless CHSPACE carries four named restrictions in its own capability reason: top-view untwisted viewports only, dimensions refused unless frozen, ACIS/proxy/table refused, and viewport clipping reported rather than applied.</sub>
> [!IMPORTANT] > [!IMPORTANT]
> Every COM path added in v1.5.0 was executed against a **live AutoCAD 2026** before release. That run found four defects no amount of reading would have caught — the largest being that `AcadPViewport` has no `ViewCenter` member, so the line setting it had been silently swallowed by a bare `except` since v1.4. > The COM paths added in v1.5.0 were executed against a **live AutoCAD 2026** before release — two documented exceptions aside: the `AcDbLWPolyline` OCS→WCS translation in `entity_get` and the COM half of `block_create_from_entities` ship unit-tested against a fake ActiveX object, not hardware-verified. The live run was 25 calls: 18 succeeded, 1 refused a specific argument, and 6 refused with the capability keys they declare. That run found four defects, three of which no amount of reading would have caught (the fourth was `analysis_list_properties` calling a helper that does not exist on `ComBackend`) — the largest being that `AcadPViewport` has no `ViewCenter` member, so the line setting it had been silently swallowed by a bare `except` since v1.4.
## Evidence ## Evidence
Every number below is produced by a script in [`benchmarks/`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/) and rendered to SVG by Python. No hand-edited charts. Every chart below is rendered to SVG by Python from committed data — none is hand-edited. The numbers are not all script output, and the two exceptions are marked where they appear: **section 4** quotes timings collected by hand across separate runs, and **section 5** is a hand-assigned source review whose nine scores are literals in [`source_review.json`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/source_review.json) that the renderer validates and draws but does not compute.
### 1 · Live-run scores ### 1 · Live-run scores
@@ -178,13 +180,13 @@ Every number below is produced by a script in [`benchmarks/`](https://github.com
> [!WARNING] > [!WARNING]
> **Read the matrix column before the score column.** The five v3 tasks are new in v1.5.0; the competitor runs are from 2026-07-23 against pinned competitor commits, and were never put to them. Their scores are over the ten tasks they actually ran, and the five new rows render blank rather than as zeros — an invented zero is indistinguishable from a measured one. These bars do not share a denominator, which is why each one carries its matrix. > **Read the matrix column before the score column.** The five v3 tasks are new in v1.5.0; the competitor runs are from 2026-07-23 against pinned competitor commits, and were never put to them. Their scores are over the ten tasks they actually ran, and the five new rows render blank rather than as zeros — an invented zero is indistinguishable from a measured one. These bars do not share a denominator, which is why each one carries its matrix.
Competitors are driven **black-box over MCP stdio** using their own documented tool contracts at the pinned commit, and every geometry claim is verified by re-opening the exported DXF with ezdxf inside the harness. No self-reporting. A capability refusal is recorded `unsupported`, not `fail` — "cannot reach this" and "got this wrong" are different findings. Competitors are driven **black-box over MCP stdio** against the tool contracts read from their pinned source, at **50% coverage on both**: five of the ten tasks per competitor are recorded `unsupported` — the operation is absent from that checkout, or refused by it — and those rows score zero against a denominator of ten, so 50.0 and 45.0 are coverage-weighted, not ten measured attempts. Over the tasks actually driven, beiming scores 100.0 and puran-water 90.0. Where a task does run, every geometry claim the competitor makes is re-derived by re-opening the exported DXF with ezdxf inside the harness — their own tool output is never the source of truth. This repo's row is not run that way: it runs in-process against the backend, and 13 of its 15 tasks assert on backend return values rather than on a re-opened DXF (only `dxf_roundtrip` and `auditable_delivery` write a file at all). What is independent there is the expected value — a closed-form area, entities placed on purpose, a token ceiling fixed before the measurement — not the observation. A capability refusal is recorded `unsupported`, not `fail` — "cannot reach this" and "got this wrong" are different findings. That classification is written into the adapter from the competitor's pinned source; a refusal that only appears at runtime becomes `unsupported` where the adapter recognises it (beiming's `E_UNSUPPORTED` on `transaction begin` is the one that did) and lands as `fail` otherwise.
### 2 · Task matrix — where the score comes from ### 2 · Task matrix — where the score comes from
<img src="https://raw.githubusercontent.com/U-C4N/Autocad-MCP/main/docs/assets/autocad-mcp-taskmatrix.svg" alt="Task matrix heatmap: 15 tasks against three servers" width="820"> <img src="https://raw.githubusercontent.com/U-C4N/Autocad-MCP/main/docs/assets/autocad-mcp-taskmatrix.svg" alt="Task matrix heatmap: 15 tasks against three servers" width="820">
**v2 scored this repo 10/10, and that number carried no information**: every task in it exercised something the server was built around. v3 adds five tasks chosen because they *can* fail — and three of them did, while being written. **v2 scored this repo 10/10, and that number carried no information**: every task in it exercised something the server was built around. v3 adds five tasks chosen because they *can* fail — and two of them did, while being written: `hatch_islands` (a HATCH could not be measured at all) and `measure_from_handle` (the boundary tools and the measurement tool disagreed about the same shape). A third measurement defect fixed in the same release — a circle stored as two bulged vertices measured 0.0 — is carried by `two_vertex_circle_has_area` in the correctness suite, not by a benchmark task.
| v3 task | Verified against | | v3 task | Verified against |
|---|---| |---|---|
@@ -218,15 +220,17 @@ The 21 pre-existing checks are unchanged and all still pass: the discovery layer
| Workload | Wall time | Result | | Workload | Wall time | Result |
|---|---:|---| |---|---:|---|
| Create 2,000 lines (individual calls) | 0.32 s | ~6,300 entities/s | | Create 2,000 lines (individual calls) | 0.35 s | ~5,716 entities/s |
| 10,000 lines: build + DXF export + reopen | 2.16 s | ~4,600 entities/s end-to-end | | 10,000 lines: build + DXF export + reopen | 2.18 s | ~4,591 entities/s end-to-end |
| Region query over 10,000 entities | 0.89 s | 2,500 matched | | Region query over 10,000 entities | 2.47 s | 2,500 matched |
| Premium pass (layers + part + dims + full critique) | 0.18 s | 0 issues | | Premium pass (layers + part + dims + full critique) | 0.26 s | 0 issues |
Workloads call the same backend methods the MCP tools call, so server-side overhead is included. **Self-measurement only** — competitor servers pay an extra stdio serialization cost that in-process runs do not, so no cross-server timing claims are made. <sub>One run of each workload — `perf_suite` executes each exactly once per invocation, and these are the figures in the committed [`perf-ezdxf.json`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/results/published/perf-ezdxf.json). Run-to-run spread on this machine is wide enough to matter: treat them as an order of magnitude, not a stopwatch.</sub>
Workloads call the same backend methods the MCP tools call, so the backend's own per-call overhead — worker-thread hand-off, document lock, the `EZDXF_CALL_TIMEOUT` guard — is included. The FastMCP layer above it is **not**: `perf_suite.py` never builds the server, so tool dispatch, argument validation, the five middleware and result serialization all sit outside the timer, and in-process they cost roughly an order of magnitude more than the backend call itself. Read these as backend-engine numbers, not per-tool-call latency. **Self-measurement only** — competitor servers pay an extra stdio serialization cost that in-process runs do not, so no cross-server timing claims are made.
> [!CAUTION] > [!CAUTION]
> These are **not** an improvement over v1.4.0. Measured against v1.4.0 on the same interpreter, this release is **28% slower** at creating entities — see [what this release is bad at](#what-this-release-is-bad-at) for the number and its full attribution. > These are **not** an improvement over v1.4.0, and they are not the engine's floor. The per-call timeout guard costs about **21%** of entity-creation throughput — see [what this release is bad at](#what-this-release-is-bad-at) for the paired measurement and why the guard is kept.
### 5 · Source-review leaderboard ### 5 · Source-review leaderboard
@@ -234,7 +238,7 @@ Workloads call the same backend methods the MCP tools call, so server-side overh
Nine public AutoCAD MCP projects against a fixed 100-point rubric — CAD breadth (25), correctness and delivery (20), backends/platforms (15), engineering production (15), tests and maintenance (15), security and operations (10). Stars and raw tool counts score nothing. Data and dates in [`benchmarks/source_review.json`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/source_review.json). Nine public AutoCAD MCP projects against a fixed 100-point rubric — CAD breadth (25), correctness and delivery (20), backends/platforms (15), engineering production (15), tests and maintenance (15), security and operations (10). Stars and raw tool counts score nothing. Data and dates in [`benchmarks/source_review.json`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/source_review.json).
**This table is not re-scored per release.** This repository's row is still the one written against **v1.4.0**. Raising our own number for v1.5.0 would mean re-reviewing eight other projects the same day to keep it fair, and that has not been done — so the score stays where the evidence is. **This table is not re-scored per release** — no row is, including the other eight. This repository's row is still the one written against **v1.4.0**. Raising our own number for v1.5.0 would mean re-reviewing eight other projects the same day to keep it fair, and that has not been done — so the score stays where the evidence is.
Method, caveats and boundaries for every lane: [`benchmarks/README.md`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/README.md). Method, caveats and boundaries for every lane: [`benchmarks/README.md`](https://github.com/U-C4N/Autocad-MCP/blob/main/benchmarks/README.md).
@@ -242,17 +246,39 @@ Method, caveats and boundaries for every lane: [`benchmarks/README.md`](https://
A page that only lists strengths is a page that has not been measured. A page that only lists strengths is a page that has not been measured.
**Entity creation is 28% slower than v1.4.0.** Measured on one interpreter, median of three runs: 2,000 lines went 245 ms → 315 ms, and the 10,000-line roundtrip 1,765 ms → 2,146 ms. Fully attributed — setting `EZDXF_CALL_TIMEOUT=0` returns v1.5.0 to **244.8 ms** and **1,679 ms**, which are v1.4.0's numbers exactly. The cost is the per-call `asyncio.wait_for` this release wrapped around every headless call so that one hung call can no longer wedge a server whose document lock is a single `asyncio.Lock`. A deliberate trade, with a documented knob, on the 1.6 roadmap to win back. **Entity creation is ~21% slower than it would be without the call timeout.**
The timeout is what stops one hung ezdxf call wedging a server whose document
lock is a single `asyncio.Lock`, and it is not free. Measured on one
interpreter, three runs of each configuration with the median taken by hand
(`perf_suite` runs each workload once per invocation — it cannot produce a
median itself):
> The v1.4 README printed *slower* numbers than the table above — 0.64 s and 13.9 s for the same two workloads. That looks like a 2–6× win and is not one: the old report was recorded on CPython 3.14 and this one on 3.11.15. Interpreter, not code. Comparing the two published files directly is the mistake this paragraph exists to stop. | Workload | `EZDXF_CALL_TIMEOUT=0` | default (120 s) | Cost |
|---|---:|---:|---:|
| 2,000 lines | 243.7 ms | 294.3 ms | +20.8% |
| 10,000-line roundtrip | 1,709.6 ms | 2,049.2 ms | +19.9% |
**Wave A shipped 13 tools of a planned 39.** Every cut is backed by a measurement. The REGION and 2D-boolean family went because `add_region()` produces a REGION with **zero ACIS bytes**, and the `greiner_hormann` substitute loses **28.2%** of the area on a square with one semicircular edge — shipping it would have re-created the exact class of silent error this release spent four milestones deleting. Most of the planned measurement family was already covered by the eight `analysis_*` tools that existed. v1.5.0 paid **28.6%** on the first workload; v1.5.1 rebuilt the wrapper around a
single `asyncio.timeout_at` instead of two `asyncio.wait_for`s — `wait_for`
wraps its awaitable in a Task, so the old shape created two extra Tasks and two
timer handles on *every* call. That recovered about a third of the overhead and
fixed a latent double-release, but it did not close the gap: the remaining cost
is the Task the abandon-on-timeout design genuinely needs. The knob is
documented rather than hidden, and the item stays open.
> These figures are hand-collected across separate single-shot runs and have no
> published artifact; `benchmarks/results/published/perf-ezdxf.json` records one
> default-configuration run only.
> The README on `main` through the v1.4 era printed *slower* numbers than the table above — 0.64 s and 13.9 s for the same two workloads. That looks like a 2–6× win and is not one: the old report was recorded on CPython 3.14 and this one on 3.11.15. Almost entirely the interpreter, not code — run on one interpreter, this release is *slower* than v1.4.0 on both workloads, not faster. Comparing the two published files directly is the mistake this paragraph exists to stop.
**Wave A shipped 13 tools of a planned ~39.** Every cut is backed by a measurement. The REGION and 2D-boolean family went because `add_region()` produces a REGION with **zero ACIS bytes**, and the substitute, `greiner_hormann`, takes and returns flat vertex lists, so a bulged edge goes in as its chord: **28.2%** of the area on a square with one semicircular edge — shipping it would have re-created the exact class of silent error this release's honesty pass exists to delete. Of the planned measurement family only `analysis_measure_entity` shipped — measuring a drawn entity by handle, which neither backend could do — and the centroid and moment-of-inertia tools were cut with nothing replacing them. Of the eight `analysis_*` tools that already existed, the two tagged `measure` both measured points the caller typed in.
**`system_run_command` / `system_run_lisp` are a guardrail, not a security boundary** — the rejection message clients receive says so in those words. A 36-verb command denylist and a 24-pattern AutoLISP denylist refuse the obviously destructive cases, but AutoCAD accepts hundreds of commands and any loaded ARX/LISP adds more, so the list cannot be complete, and `DANGEROUS_COMMANDS_ENABLED=true` switches both off entirely. **`system_run_command` / `system_run_lisp` are a guardrail, not a security boundary** — the rejection message clients receive says so in those words. A 36-verb command denylist and a 24-pattern AutoLISP denylist refuse the obviously destructive cases, but AutoCAD accepts hundreds of commands and any loaded ARX/LISP adds more, so the list cannot be complete, and `DANGEROUS_COMMANDS_ENABLED=true` switches both off entirely.
**Path validation is per-tool, and unscoped until you scope it.** Nine tools call `validate_path`; nothing in the middleware enforces it server-wide. With `ALLOWED_PATHS` empty — the default — the only positive bound is a ten-entry system-directory denylist that does not include `C:/Users`, `/home`, `/root` or `/var`. **Set `ALLOWED_PATHS`.** It is the setting that actually scopes the filesystem. **Path validation is per-tool, and unscoped until you scope it.** Nine tools call `validate_path`; nothing in the middleware enforces it server-wide. With `ALLOWED_PATHS` empty — the default — the only positive bound is a ten-entry system-directory denylist that does not include `C:/Users`, `/home`, `/root` or `/var`. **Set `ALLOWED_PATHS`.** It is the setting that actually scopes the filesystem.
**Non-AutoCAD ProgIDs are unverified.** `CAD_PROGID` changes which COM application the backend attaches to, and nothing beyond the connection has been tested against BricsCAD, ZWCAD or GstarCAD. They are listed in `.env.example` commented out and marked UNVERIFIED. **Non-AutoCAD ProgIDs are unverified.** `CAD_PROGID` changes which COM application the backend attaches to — the configured ProgID is used at both `GetActiveObject` and `Dispatch`, and an unrecognised one fails loudly rather than falling back to AutoCAD. Nothing has been tested against BricsCAD, ZWCAD or GstarCAD, the connection included: no running seat of any of them was ever attached to. They are listed in `.env.example` commented out and marked UNVERIFIED.
## Configuration ## Configuration
+43 -32
View File
@@ -60,7 +60,7 @@ written:
| Task | Category | Verified against | | Task | Category | Verified against |
|---|---|---| |---|---|---|
| `tool_discovery` | discovery | six AutoCAD command names, each ranking #1 of 154 tools | | `tool_discovery` | discovery | six AutoCAD command names, each found in the top 3 of the 154-tool registry the harness ranks — all six came back #1 in the published run |
| `token_budget` | efficiency | 40,305 → 356 tokens advertised, against a 2,000 ceiling fixed in advance | | `token_budget` | efficiency | 40,305 → 356 tokens advertised, against a 2,000 ceiling fixed in advance |
| `hatch_islands` | hatch | 300 filled with the island, 400 ignoring it | | `hatch_islands` | hatch | 300 filled with the island, 400 ignoring it |
| `selection_filter` | selection | window 1, crossing 2, bounding box 3, polygon 1 | | `selection_filter` | selection | window 1, crossing 2, bounding box 3, polygon 1 |
@@ -153,33 +153,36 @@ python -m benchmarks.perf_suite --out benchmarks/results/published/perf-ezdxf.js
python -m benchmarks.render_perf_chart python -m benchmarks.render_perf_chart
``` ```
### v1.4.0 → v1.5.0, same interpreter ### What the call-timeout guard costs
The published v1.4 report was recorded on CPython 3.14 and the v1.5 one on The published v1.4 report was recorded on CPython 3.14 and the later ones on
3.11.15, so comparing the two files directly shows an apparent 2–6× speedup 3.11.15, so comparing those two files directly shows an apparent 2–6× speedup
that is almost entirely the interpreter. The honest comparison re-runs v1.4.0 that is almost entirely the interpreter. The comparison that means something is
in a `git worktree` with the *current* suite file — same instrument, different the guard against no guard, on one interpreter, back to back. Three runs of
code under test, one interpreter. Median of three runs each: each configuration, median taken by hand — `perf_suite` executes each workload
once per invocation and cannot produce a median itself:
| Workload | v1.4.0 | v1.5.0 | Change | | Workload | `EZDXF_CALL_TIMEOUT=0` | default (120 s) | Cost |
|---|---:|---:|---| |---|---:|---:|---:|
| `create_lines_2k` | 245 ms | 315 ms | **28% slower** | | `create_lines_2k` | 243.7 ms | 294.3 ms | **+20.8%** |
| `roundtrip_10k` | 1,765 ms | 2,146 ms | **22% slower** | | `roundtrip_10k` | 1,709.6 ms | 2,049.2 ms | **+19.9%** |
| `region_query_10k` | 1,090 ms | 1,039 ms | within spread |
| `premium_pass` | 375 ms | 44 ms | faster; the v1.4 spread (279–772 ms) is too wide to quantify |
Attributed rather than reported: `EZDXF_CALL_TIMEOUT=0` puts v1.5.0 back to v1.5.0 paid 28.6% on the first workload; v1.5.1 rebuilt the wrapper around a
244.8 ms and 1,679 ms, which are v1.4.0's numbers. The entire cost is the single `asyncio.timeout_at` instead of two `asyncio.wait_for`s — `wait_for`
per-call `asyncio.wait_for` wrapped around every headless call in this release, wraps its awaitable in a Task, so the old shape created two extra Tasks and two
so a single hung ezdxf call can no longer wedge a server whose document lock is timer handles per call — which recovered about a third of the overhead. The
one `asyncio.Lock`. Reproduce: rest is the Task the abandon-on-timeout design genuinely needs. The guard is
what stops one hung ezdxf call wedging a server whose document lock is a single
`asyncio.Lock`, so it stays; the knob is documented rather than hidden.
These figures are hand-collected and have **no published artifact** —
`perf-ezdxf.json` records one default-configuration run only. Reproduce:
```bash ```bash
git worktree add /tmp/wt-v140 v1.4.0 for i in 1 2 3; do
cp benchmarks/perf_suite.py /tmp/wt-v140/benchmarks/ python -m benchmarks.perf_suite --out /tmp/on-$i.json
(cd /tmp/wt-v140 && PYTHONPATH=. python benchmarks/perf_suite.py --out /tmp/v140.json) EZDXF_CALL_TIMEOUT=0 python -m benchmarks.perf_suite --out /tmp/off-$i.json
python -m benchmarks.perf_suite --out /tmp/v150.json done
EZDXF_CALL_TIMEOUT=0 python -m benchmarks.perf_suite --out /tmp/v150-notimeout.json
git worktree remove --force /tmp/wt-v140 git worktree remove --force /tmp/wt-v140
``` ```
@@ -201,20 +204,28 @@ python benchmarks/compare_versions.py --json results.json
### Result — v1.5.0 vs v1.4.0 (release gate) ### Result — v1.5.0 vs v1.4.0 (release gate)
24 checks, ezdxf backend, one subprocess per check. Machine-readable report: 26 checks, ezdxf backend, one subprocess per check. Machine-readable report:
[`results/published/ab-v1.4.0-vs-v1.5.0.json`](results/published/ab-v1.4.0-vs-v1.5.0.json). [`results/published/ab-v1.4.0-vs-v1.5.0.json`](results/published/ab-v1.4.0-vs-v1.5.0.json).
| Version | Checks passing | Pass rate | Fixed | Regressed | | Version | Checks passing | Pass rate | Fixed | Regressed |
|---------|----------------|-----------|-------|-----------| |---------|----------------|-----------|-------|-----------|
| **v1.4.0** (baseline) | 21 / 24 | 87.5 % | — | — | | **v1.4.0** (baseline) | 21 / 26 | 80.8 % | — | — |
| **v1.5.0** (this release) | 24 / 24 | 100 % | 3 | **0** | | **v1.5.0** (this release) | 26 / 26 | 100 % | 5 | **0** |
Read the three "fixed" carefully: they are **new capability, not repaired The pass rate is against the 26-check v1.5.0 suite; v1.4.0 passed 21/21 of the
regressions**. v1.4.0 has no `entity_measure` and no boundary tracing, so it checks that existed when it shipped. The five splits into two kinds, and the
misses `hatch_area_subtracts_its_island`, `boundary_area_agrees_with_measure` difference is the whole reason this lane distinguishes `miss` from `fail`:
and `two_vertex_circle_has_area` by not having the method rather than by
getting it wrong. They are in the suite because all three broke during v1.5.0 * **Three are new capability.** v1.4.0 has no `entity_measure` and no boundary
development and nothing outside their own unit tests would have caught it. tracing, so it misses `hatch_area_subtracts_its_island`,
`boundary_area_agrees_with_measure` and `two_vertex_circle_has_area` by not
having the method (`miss → pass`). They are in the suite because all three
broke during v1.5.0 development and nothing outside their own unit tests
would have caught it.
* **Two are repaired defects.** v1.4.0 *has* `dimension_diameter` and
`dimension_radius` and gets them wrong (`fail → pass`): `leader_length`, a
text placement, was being measured as geometry, so at default settings every
diameter callout came out 2 × `leader_length` too large.
The 21 pre-existing checks are unchanged and all still pass: the discovery The 21 pre-existing checks are unchanged and all still pass: the discovery
layer, `cad_batch`, the 19-module contract split, the layout/viewport family layer, `cad_batch`, the 19-module contract split, the layout/viewport family
+9 -9
View File
@@ -14,22 +14,22 @@
"entity_count": 2000 "entity_count": 2000
}, },
"metric": "entities_per_second", "metric": "entities_per_second",
"metric_value": 6343.1, "metric_value": 5715.8,
"wall_ms": 315.3, "wall_ms": 349.9,
"workload_id": "create_lines_2k" "workload_id": "create_lines_2k"
}, },
{ {
"description": "Create 10,000 lines, export DXF, reopen and verify", "description": "Create 10,000 lines, export DXF, reopen and verify",
"detail": { "detail": {
"build_ms": 1574.1, "build_ms": 1633.1,
"entity_count": 10000, "entity_count": 10000,
"export_ms": 261.8, "export_ms": 273.0,
"file_bytes": 1369663, "file_bytes": 1369663,
"reopen_ms": 320.2 "reopen_ms": 272.1
}, },
"metric": "entities_per_second", "metric": "entities_per_second",
"metric_value": 4637.9, "metric_value": 4590.8,
"wall_ms": 2156.1, "wall_ms": 2178.3,
"workload_id": "roundtrip_10k" "workload_id": "roundtrip_10k"
}, },
{ {
@@ -39,7 +39,7 @@
}, },
"metric": "entities_matched", "metric": "entities_matched",
"metric_value": 2500.0, "metric_value": 2500.0,
"wall_ms": 890.8, "wall_ms": 2466.9,
"workload_id": "region_query_10k" "workload_id": "region_query_10k"
}, },
{ {
@@ -49,7 +49,7 @@
}, },
"metric": "critique_issues", "metric": "critique_issues",
"metric_value": 0.0, "metric_value": 0.0,
"wall_ms": 176.5, "wall_ms": 255.7,
"workload_id": "premium_pass" "workload_id": "premium_pass"
} }
] ]
+313 -308
View File
@@ -39,8 +39,8 @@ z
<g id="matplotlib.axis_1"> <g id="matplotlib.axis_1">
<g id="xtick_1"> <g id="xtick_1">
<g id="line2d_1"> <g id="line2d_1">
<path d="M 455.882104 273.200579 <path d="M 424.840312 273.200579
L 455.882104 44.677427 L 424.840312 44.677427
" clip-path="url(#p16cc1d88db)" style="fill: none; stroke: #30363d; stroke-opacity: 0.8; stroke-width: 0.8; stroke-linecap: square"/> " clip-path="url(#p16cc1d88db)" style="fill: none; stroke: #30363d; stroke-opacity: 0.8; stroke-width: 0.8; stroke-linecap: square"/>
</g> </g>
<g id="line2d_2"> <g id="line2d_2">
@@ -50,12 +50,12 @@ L 0 3.5
" style="stroke: #8b949e; stroke-width: 0.8"/> " style="stroke: #8b949e; stroke-width: 0.8"/>
</defs> </defs>
<g> <g>
<use xlink:href="#m24a80d22c9" x="455.882104" y="273.200579" style="fill: #8b949e; stroke: #8b949e; stroke-width: 0.8"/> <use xlink:href="#m24a80d22c9" x="424.840312" y="273.200579" style="fill: #8b949e; stroke: #8b949e; stroke-width: 0.8"/>
</g> </g>
</g> </g>
<g id="text_1"> <g id="text_1">
<!-- $\mathdefault{10^{3}}$ --> <!-- $\mathdefault{10^{3}}$ -->
<g style="fill: #8b949e" transform="translate(447.082104 289.600579) scale(0.1 -0.1)"> <g style="fill: #8b949e" transform="translate(416.040312 289.600579) scale(0.1 -0.1)">
<defs> <defs>
<path id="DejaVuSans-14" d="M 794 531 <path id="DejaVuSans-14" d="M 794 531
L 1825 531 L 1825 531
@@ -133,18 +133,18 @@ z
</g> </g>
<g id="xtick_2"> <g id="xtick_2">
<g id="line2d_3"> <g id="line2d_3">
<path d="M 693.819287 273.200579 <path d="M 676.0002 273.200579
L 693.819287 44.677427 L 676.0002 44.677427
" clip-path="url(#p16cc1d88db)" style="fill: none; stroke: #30363d; stroke-opacity: 0.8; stroke-width: 0.8; stroke-linecap: square"/> " clip-path="url(#p16cc1d88db)" style="fill: none; stroke: #30363d; stroke-opacity: 0.8; stroke-width: 0.8; stroke-linecap: square"/>
</g> </g>
<g id="line2d_4"> <g id="line2d_4">
<g> <g>
<use xlink:href="#m24a80d22c9" x="693.819287" y="273.200579" style="fill: #8b949e; stroke: #8b949e; stroke-width: 0.8"/> <use xlink:href="#m24a80d22c9" x="676.0002" y="273.200579" style="fill: #8b949e; stroke: #8b949e; stroke-width: 0.8"/>
</g> </g>
</g> </g>
<g id="text_2"> <g id="text_2">
<!-- $\mathdefault{10^{4}}$ --> <!-- $\mathdefault{10^{4}}$ -->
<g style="fill: #8b949e" transform="translate(685.019287 289.500579) scale(0.1 -0.1)"> <g style="fill: #8b949e" transform="translate(667.2002 289.500579) scale(0.1 -0.1)">
<defs> <defs>
<path id="DejaVuSans-17" d="M 2419 4116 <path id="DejaVuSans-17" d="M 2419 4116
L 825 1625 L 825 1625
@@ -180,112 +180,105 @@ L 0 2
" style="stroke: #000000; stroke-width: 0.6"/> " style="stroke: #000000; stroke-width: 0.6"/>
</defs> </defs>
<g> <g>
<use xlink:href="#m1face5d2a1" x="289.57115" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="293.514145" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_4"> <g id="xtick_4">
<g id="line2d_6"> <g id="line2d_6">
<g> <g>
<use xlink:href="#m1face5d2a1" x="331.469808" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="324.893745" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_5"> <g id="xtick_5">
<g id="line2d_7"> <g id="line2d_7">
<g> <g>
<use xlink:href="#m1face5d2a1" x="361.197379" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="349.233652" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_6"> <g id="xtick_6">
<g id="line2d_8"> <g id="line2d_8">
<g> <g>
<use xlink:href="#m1face5d2a1" x="384.255874" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="369.120805" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_7"> <g id="xtick_7">
<g id="line2d_9"> <g id="line2d_9">
<g> <g>
<use xlink:href="#m1face5d2a1" x="403.096037" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="385.935154" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_8"> <g id="xtick_8">
<g id="line2d_10"> <g id="line2d_10">
<g> <g>
<use xlink:href="#m1face5d2a1" x="419.025168" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="400.500404" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_9"> <g id="xtick_9">
<g id="line2d_11"> <g id="line2d_11">
<g> <g>
<use xlink:href="#m1face5d2a1" x="432.823608" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="413.347866" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_10"> <g id="xtick_10">
<g id="line2d_12"> <g id="line2d_12">
<g> <g>
<use xlink:href="#m1face5d2a1" x="444.994695" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="500.446972" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_11"> <g id="xtick_11">
<g id="line2d_13"> <g id="line2d_13">
<g> <g>
<use xlink:href="#m1face5d2a1" x="527.508333" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="544.674033" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_12"> <g id="xtick_12">
<g id="line2d_14"> <g id="line2d_14">
<g> <g>
<use xlink:href="#m1face5d2a1" x="569.406991" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="576.053632" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_13"> <g id="xtick_13">
<g id="line2d_15"> <g id="line2d_15">
<g> <g>
<use xlink:href="#m1face5d2a1" x="599.134562" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="600.39354" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_14"> <g id="xtick_14">
<g id="line2d_16"> <g id="line2d_16">
<g> <g>
<use xlink:href="#m1face5d2a1" x="622.193058" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="620.280693" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_15"> <g id="xtick_15">
<g id="line2d_17"> <g id="line2d_17">
<g> <g>
<use xlink:href="#m1face5d2a1" x="641.033221" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="637.095041" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_16"> <g id="xtick_16">
<g id="line2d_18"> <g id="line2d_18">
<g> <g>
<use xlink:href="#m1face5d2a1" x="656.962351" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="651.660292" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
<g id="xtick_17"> <g id="xtick_17">
<g id="line2d_19"> <g id="line2d_19">
<g> <g>
<use xlink:href="#m1face5d2a1" x="670.760792" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/> <use xlink:href="#m1face5d2a1" x="664.507754" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g>
</g>
</g>
<g id="xtick_18">
<g id="line2d_20">
<g>
<use xlink:href="#m1face5d2a1" x="682.931879" y="273.200579" style="stroke: #000000; stroke-width: 0.6"/>
</g> </g>
</g> </g>
</g> </g>
@@ -708,7 +701,7 @@ z
</g> </g>
<g id="matplotlib.axis_2"> <g id="matplotlib.axis_2">
<g id="ytick_1"> <g id="ytick_1">
<g id="line2d_21"/> <g id="line2d_20"/>
<g id="text_4"> <g id="text_4">
<!-- Premium pass: layers + part + dims + full critique --> <!-- Premium pass: layers + part + dims + full critique -->
<g style="fill: #e6edf3" transform="translate(3.00024 249.783608) scale(0.1 -0.1)"> <g style="fill: #e6edf3" transform="translate(3.00024 249.783608) scale(0.1 -0.1)">
@@ -955,7 +948,7 @@ z
</g> </g>
</g> </g>
<g id="ytick_2"> <g id="ytick_2">
<g id="line2d_22"/> <g id="line2d_21"/>
<g id="text_5"> <g id="text_5">
<!-- Create 2,000 lines (individual calls) --> <!-- Create 2,000 lines (individual calls) -->
<g style="fill: #e6edf3" transform="translate(79.172115 191.753351) scale(0.1 -0.1)"> <g style="fill: #e6edf3" transform="translate(79.172115 191.753351) scale(0.1 -0.1)">
@@ -1066,81 +1059,10 @@ z
</g> </g>
</g> </g>
<g id="ytick_3"> <g id="ytick_3">
<g id="line2d_23"/> <g id="line2d_22"/>
<g id="text_6"> <g id="text_6">
<!-- Region query over 10,000 entities -->
<g style="fill: #e6edf3" transform="translate(85.364303 133.723094) scale(0.1 -0.1)">
<defs>
<path id="DejaVuSans-35" d="M 2841 2188
Q 3044 2119 3236 1894
Q 3428 1669 3622 1275
L 4263 0
L 3584 0
L 2988 1197
Q 2756 1666 2539 1819
Q 2322 1972 1947 1972
L 1259 1972
L 1259 0
L 628 0
L 628 4666
L 2053 4666
Q 2853 4666 3247 4331
Q 3641 3997 3641 3322
Q 3641 2881 3436 2590
Q 3231 2300 2841 2188
z
M 1259 4147
L 1259 2491
L 2053 2491
Q 2509 2491 2742 2702
Q 2975 2913 2975 3322
Q 2975 3731 2742 3939
Q 2509 4147 2053 4147
L 1259 4147
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-35"/>
<use xlink:href="#DejaVuSans-48" transform="translate(65 0)"/>
<use xlink:href="#DejaVuSans-4a" transform="translate(126.53125 0)"/>
<use xlink:href="#DejaVuSans-4c" transform="translate(190.015625 0)"/>
<use xlink:href="#DejaVuSans-52" transform="translate(217.796875 0)"/>
<use xlink:href="#DejaVuSans-51" transform="translate(278.984375 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(342.359375 0)"/>
<use xlink:href="#DejaVuSans-54" transform="translate(374.140625 0)"/>
<use xlink:href="#DejaVuSans-58" transform="translate(437.625 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(501 0)"/>
<use xlink:href="#DejaVuSans-55" transform="translate(562.53125 0)"/>
<use xlink:href="#DejaVuSans-5c" transform="translate(603.640625 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(662.828125 0)"/>
<use xlink:href="#DejaVuSans-52" transform="translate(694.609375 0)"/>
<use xlink:href="#DejaVuSans-59" transform="translate(755.796875 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(814.984375 0)"/>
<use xlink:href="#DejaVuSans-55" transform="translate(876.515625 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(917.625 0)"/>
<use xlink:href="#DejaVuSans-14" transform="translate(949.40625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1013.03125 0)"/>
<use xlink:href="#DejaVuSans-f" transform="translate(1076.65625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1108.4375 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1172.0625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1235.6875 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(1299.3125 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(1331.09375 0)"/>
<use xlink:href="#DejaVuSans-51" transform="translate(1392.625 0)"/>
<use xlink:href="#DejaVuSans-57" transform="translate(1456 0)"/>
<use xlink:href="#DejaVuSans-4c" transform="translate(1495.203125 0)"/>
<use xlink:href="#DejaVuSans-57" transform="translate(1522.984375 0)"/>
<use xlink:href="#DejaVuSans-4c" transform="translate(1562.1875 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(1589.96875 0)"/>
<use xlink:href="#DejaVuSans-56" transform="translate(1651.5 0)"/>
</g>
</g>
</g>
<g id="ytick_4">
<g id="line2d_24"/>
<g id="text_7">
<!-- 10,000 lines: build + DXF export + reopen --> <!-- 10,000 lines: build + DXF export + reopen -->
<g style="fill: #e6edf3" transform="translate(42.81274 75.692837) scale(0.1 -0.1)"> <g style="fill: #e6edf3" transform="translate(42.81274 133.723094) scale(0.1 -0.1)">
<defs> <defs>
<path id="DejaVuSans-27" d="M 1259 4147 <path id="DejaVuSans-27" d="M 1259 4147
L 1259 519 L 1259 519
@@ -1249,36 +1171,107 @@ z
</g> </g>
</g> </g>
</g> </g>
<g id="ytick_4">
<g id="line2d_23"/>
<g id="text_7">
<!-- Region query over 10,000 entities -->
<g style="fill: #e6edf3" transform="translate(85.364303 75.692837) scale(0.1 -0.1)">
<defs>
<path id="DejaVuSans-35" d="M 2841 2188
Q 3044 2119 3236 1894
Q 3428 1669 3622 1275
L 4263 0
L 3584 0
L 2988 1197
Q 2756 1666 2539 1819
Q 2322 1972 1947 1972
L 1259 1972
L 1259 0
L 628 0
L 628 4666
L 2053 4666
Q 2853 4666 3247 4331
Q 3641 3997 3641 3322
Q 3641 2881 3436 2590
Q 3231 2300 2841 2188
z
M 1259 4147
L 1259 2491
L 2053 2491
Q 2509 2491 2742 2702
Q 2975 2913 2975 3322
Q 2975 3731 2742 3939
Q 2509 4147 2053 4147
L 1259 4147
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-35"/>
<use xlink:href="#DejaVuSans-48" transform="translate(65 0)"/>
<use xlink:href="#DejaVuSans-4a" transform="translate(126.53125 0)"/>
<use xlink:href="#DejaVuSans-4c" transform="translate(190.015625 0)"/>
<use xlink:href="#DejaVuSans-52" transform="translate(217.796875 0)"/>
<use xlink:href="#DejaVuSans-51" transform="translate(278.984375 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(342.359375 0)"/>
<use xlink:href="#DejaVuSans-54" transform="translate(374.140625 0)"/>
<use xlink:href="#DejaVuSans-58" transform="translate(437.625 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(501 0)"/>
<use xlink:href="#DejaVuSans-55" transform="translate(562.53125 0)"/>
<use xlink:href="#DejaVuSans-5c" transform="translate(603.640625 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(662.828125 0)"/>
<use xlink:href="#DejaVuSans-52" transform="translate(694.609375 0)"/>
<use xlink:href="#DejaVuSans-59" transform="translate(755.796875 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(814.984375 0)"/>
<use xlink:href="#DejaVuSans-55" transform="translate(876.515625 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(917.625 0)"/>
<use xlink:href="#DejaVuSans-14" transform="translate(949.40625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1013.03125 0)"/>
<use xlink:href="#DejaVuSans-f" transform="translate(1076.65625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1108.4375 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1172.0625 0)"/>
<use xlink:href="#DejaVuSans-13" transform="translate(1235.6875 0)"/>
<use xlink:href="#DejaVuSans-3" transform="translate(1299.3125 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(1331.09375 0)"/>
<use xlink:href="#DejaVuSans-51" transform="translate(1392.625 0)"/>
<use xlink:href="#DejaVuSans-57" transform="translate(1456 0)"/>
<use xlink:href="#DejaVuSans-4c" transform="translate(1495.203125 0)"/>
<use xlink:href="#DejaVuSans-57" transform="translate(1522.984375 0)"/>
<use xlink:href="#DejaVuSans-4c" transform="translate(1562.1875 0)"/>
<use xlink:href="#DejaVuSans-48" transform="translate(1589.96875 0)"/>
<use xlink:href="#DejaVuSans-56" transform="translate(1651.5 0)"/>
</g>
</g>
</g>
</g> </g>
<g id="patch_3"> <g id="patch_3">
<path d="M -238195.113004 262.813163 <path d="M -251488.526977 262.813163
L 276.654662 262.813163 L 276.086029 262.813163
L 276.654662 229.155614 L 276.086029 229.155614
L -238195.113004 229.155614 L -251488.526977 229.155614
z z
" clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/> " clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/>
</g> </g>
<g id="patch_4"> <g id="patch_4">
<path d="M -238195.113004 204.782906 <path d="M -251488.526977 204.782906
L 336.609897 204.782906 L 310.297324 204.782906
L 336.609897 171.125357 L 310.297324 171.125357
L -238195.113004 171.125357 L -251488.526977 171.125357
z z
" clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/> " clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/>
</g> </g>
<g id="patch_5"> <g id="patch_5">
<path d="M -238195.113004 146.75265 <path d="M -251488.526977 146.75265
L 443.932948 146.75265 L 509.761914 146.75265
L 443.932948 113.0951 L 509.761914 113.0951
L -238195.113004 113.0951 L -251488.526977 113.0951
z z
" clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/> " clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/>
</g> </g>
<g id="patch_6"> <g id="patch_6">
<path d="M -238195.113004 88.722393 <path d="M -251488.526977 88.722393
L 535.274342 88.722393 L 523.33305 88.722393
L 535.274342 55.064843 L 523.33305 55.064843
L -238195.113004 55.064843 L -251488.526977 55.064843
z z
" clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/> " clip-path="url(#p16cc1d88db)" style="fill: #58a6ff"/>
</g> </g>
@@ -1554,8 +1547,8 @@ z
</g> </g>
</g> </g>
<g id="text_9"> <g id="text_9">
<!-- 0.18 s · 0 issues --> <!-- 0.26 s · 0 issues -->
<g style="fill: #e6edf3" transform="translate(288.365459 248.322631) scale(0.09 -0.09)"> <g style="fill: #e6edf3" transform="translate(288.447622 248.322631) scale(0.09 -0.09)">
<defs> <defs>
<path id="DejaVuSans-Bold-13" d="M 2944 2338 <path id="DejaVuSans-Bold-13" d="M 2944 2338
Q 2944 3213 2780 3570 Q 2944 3213 2780 3570
@@ -1585,57 +1578,56 @@ L 653 0
L 653 1209 L 653 1209
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-14" d="M 750 831 <path id="DejaVuSans-Bold-15" d="M 1844 884
L 1813 831 L 3897 884
L 1813 3847 L 3897 0
L 722 3622 L 506 0
L 722 4441 L 506 884
L 1806 4666 L 2209 2388
L 2950 4666 Q 2438 2594 2547 2791
L 2950 831 Q 2656 2988 2656 3200
L 4013 831 Q 2656 3528 2436 3728
L 4013 0 Q 2216 3928 1850 3928
L 750 0 Q 1569 3928 1234 3808
L 750 831 Q 900 3688 519 3450
L 519 4475
Q 925 4609 1322 4679
Q 1719 4750 2100 4750
Q 2938 4750 3402 4381
Q 3866 4013 3866 3353
Q 3866 2972 3669 2642
Q 3472 2313 2841 1759
L 1844 884
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-1b" d="M 2228 2088 <path id="DejaVuSans-Bold-19" d="M 2316 2303
Q 1891 2088 1709 1903 Q 2000 2303 1842 2098
Q 1528 1719 1528 1375 Q 1684 1894 1684 1484
Q 1528 1031 1709 848 Q 1684 1075 1842 870
Q 1891 666 2228 666 Q 2000 666 2316 666
Q 2563 666 2741 848 Q 2634 666 2792 870
Q 2919 1031 2919 1375 Q 2950 1075 2950 1484
Q 2919 1722 2741 1905 Q 2950 1894 2792 2098
Q 2563 2088 2228 2088 Q 2634 2303 2316 2303
z z
M 1350 2484 M 3803 4544
Q 925 2613 709 2878 L 3803 3681
Q 494 3144 494 3541 Q 3506 3822 3243 3889
Q 494 4131 934 4440 Q 2981 3956 2731 3956
Q 1375 4750 2228 4750 Q 2194 3956 1894 3657
Q 3075 4750 3515 4442 Q 1594 3359 1544 2772
Q 3956 4134 3956 3541 Q 1750 2925 1990 3001
Q 3956 3144 3739 2878 Q 2231 3078 2516 3078
Q 3522 2613 3097 2484 Q 3231 3078 3670 2659
Q 3572 2353 3814 2058 Q 4109 2241 4109 1563
Q 4056 1763 4056 1313 Q 4109 813 3618 361
Q 4056 619 3595 264 Q 3128 -91 2303 -91
Q 3134 -91 2228 -91 Q 1394 -91 895 523
Q 1319 -91 855 264 Q 397 1138 397 2266
Q 391 619 391 1313 Q 397 3422 980 4083
Q 391 1763 633 2058 Q 1563 4744 2578 4744
Q 875 2353 1350 2484 Q 2900 4744 3203 4694
z Q 3506 4644 3803 4544
M 1631 3419
Q 1631 3141 1786 2991
Q 1941 2841 2228 2841
Q 2509 2841 2662 2991
Q 2816 3141 2816 3419
Q 2816 3697 2662 3845
Q 2509 3994 2228 3994
Q 1941 3994 1786 3844
Q 1631 3694 1631 3419
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-3" transform="scale(0.015625)"/> <path id="DejaVuSans-Bold-3" transform="scale(0.015625)"/>
@@ -1741,8 +1733,8 @@ z
</defs> </defs>
<use xlink:href="#DejaVuSans-Bold-13"/> <use xlink:href="#DejaVuSans-Bold-13"/>
<use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/> <use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/>
<use xlink:href="#DejaVuSans-Bold-14" transform="translate(107.5625 0)"/> <use xlink:href="#DejaVuSans-Bold-15" transform="translate(107.5625 0)"/>
<use xlink:href="#DejaVuSans-Bold-1b" transform="translate(177.140625 0)"/> <use xlink:href="#DejaVuSans-Bold-19" transform="translate(177.140625 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/> <use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/>
@@ -1761,8 +1753,8 @@ z
</g> </g>
</g> </g>
<g id="text_10"> <g id="text_10">
<!-- 0.32 s · 6,343 entities/s --> <!-- 0.35 s · 5,716 entities/s -->
<g style="fill: #e6edf3" transform="translate(348.320695 190.292374) scale(0.09 -0.09)"> <g style="fill: #e6edf3" transform="translate(322.658917 190.292374) scale(0.09 -0.09)">
<defs> <defs>
<path id="DejaVuSans-Bold-16" d="M 2981 2516 <path id="DejaVuSans-Bold-16" d="M 2981 2516
Q 3453 2394 3698 2092 Q 3453 2394 3698 2092
@@ -1796,56 +1788,29 @@ Q 3834 3144 3618 2883
Q 3403 2622 2981 2516 Q 3403 2622 2981 2516
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-15" d="M 1844 884 <path id="DejaVuSans-Bold-18" d="M 678 4666
L 3897 884 L 3669 4666
L 3897 0 L 3669 3781
L 506 0 L 1638 3781
L 506 884 L 1638 3059
L 2209 2388 Q 1775 3097 1914 3117
Q 2438 2594 2547 2791 Q 2053 3138 2203 3138
Q 2656 2988 2656 3200 Q 3056 3138 3531 2711
Q 2656 3528 2436 3728 Q 4006 2284 4006 1522
Q 2216 3928 1850 3928 Q 4006 766 3489 337
Q 1569 3928 1234 3808 Q 2972 -91 2053 -91
Q 900 3688 519 3450 Q 1656 -91 1267 -14
L 519 4475 Q 878 63 494 219
Q 925 4609 1322 4679 L 494 1166
Q 1719 4750 2100 4750 Q 875 947 1217 837
Q 2938 4750 3402 4381 Q 1559 728 1863 728
Q 3866 4013 3866 3353 Q 2300 728 2551 942
Q 3866 2972 3669 2642 Q 2803 1156 2803 1522
Q 3472 2313 2841 1759 Q 2803 1891 2551 2103
L 1844 884 Q 2300 2316 1863 2316
z Q 1603 2316 1309 2248
" transform="scale(0.015625)"/> Q 1016 2181 678 2041
<path id="DejaVuSans-Bold-19" d="M 2316 2303 L 678 4666
Q 2000 2303 1842 2098
Q 1684 1894 1684 1484
Q 1684 1075 1842 870
Q 2000 666 2316 666
Q 2634 666 2792 870
Q 2950 1075 2950 1484
Q 2950 1894 2792 2098
Q 2634 2303 2316 2303
z
M 3803 4544
L 3803 3681
Q 3506 3822 3243 3889
Q 2981 3956 2731 3956
Q 2194 3956 1894 3657
Q 1594 3359 1544 2772
Q 1750 2925 1990 3001
Q 2231 3078 2516 3078
Q 3231 3078 3670 2659
Q 4109 2241 4109 1563
Q 4109 813 3618 361
Q 3128 -91 2303 -91
Q 1394 -91 895 523
Q 397 1138 397 2266
Q 397 3422 980 4083
Q 1563 4744 2578 4744
Q 2900 4744 3203 4694
Q 3506 4644 3803 4544
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-f" d="M 653 1209 <path id="DejaVuSans-Bold-f" d="M 653 1209
@@ -1857,23 +1822,28 @@ L 653 256
L 653 1209 L 653 1209
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-17" d="M 2356 3675 <path id="DejaVuSans-Bold-1a" d="M 428 4666
L 1038 1722 L 3944 4666
L 2356 1722 L 3944 3988
L 2356 3675 L 2125 0
L 953 0
L 2675 3781
L 428 3781
L 428 4666
z z
M 2156 4666 " transform="scale(0.015625)"/>
L 3494 4666 <path id="DejaVuSans-Bold-14" d="M 750 831
L 3494 1722 L 1813 831
L 4159 1722 L 1813 3847
L 4159 850 L 722 3622
L 3494 850 L 722 4441
L 3494 0 L 1806 4666
L 2356 0 L 2950 4666
L 2356 850 L 2950 831
L 288 850 L 4013 831
L 288 1881 L 4013 0
L 2156 4666 L 750 0
L 750 831
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-51" d="M 4056 2131 <path id="DejaVuSans-Bold-51" d="M 4056 2131
@@ -1930,7 +1900,7 @@ z
<use xlink:href="#DejaVuSans-Bold-13"/> <use xlink:href="#DejaVuSans-Bold-13"/>
<use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/> <use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/>
<use xlink:href="#DejaVuSans-Bold-16" transform="translate(107.5625 0)"/> <use xlink:href="#DejaVuSans-Bold-16" transform="translate(107.5625 0)"/>
<use xlink:href="#DejaVuSans-Bold-15" transform="translate(177.140625 0)"/> <use xlink:href="#DejaVuSans-Bold-18" transform="translate(177.140625 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/> <use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/>
@@ -1938,11 +1908,11 @@ z
<use xlink:href="#DejaVuSans-Bold-79" transform="translate(410.671875 0)"/> <use xlink:href="#DejaVuSans-Bold-79" transform="translate(410.671875 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(448.65625 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(448.65625 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(483.46875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(483.46875 0)"/>
<use xlink:href="#DejaVuSans-Bold-19" transform="translate(518.28125 0)"/> <use xlink:href="#DejaVuSans-Bold-18" transform="translate(518.28125 0)"/>
<use xlink:href="#DejaVuSans-Bold-f" transform="translate(587.859375 0)"/> <use xlink:href="#DejaVuSans-Bold-f" transform="translate(587.859375 0)"/>
<use xlink:href="#DejaVuSans-Bold-16" transform="translate(625.84375 0)"/> <use xlink:href="#DejaVuSans-Bold-1a" transform="translate(625.84375 0)"/>
<use xlink:href="#DejaVuSans-Bold-17" transform="translate(695.421875 0)"/> <use xlink:href="#DejaVuSans-Bold-14" transform="translate(695.421875 0)"/>
<use xlink:href="#DejaVuSans-Bold-16" transform="translate(765 0)"/> <use xlink:href="#DejaVuSans-Bold-19" transform="translate(765 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(834.578125 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(834.578125 0)"/>
<use xlink:href="#DejaVuSans-Bold-48" transform="translate(869.390625 0)"/> <use xlink:href="#DejaVuSans-Bold-48" transform="translate(869.390625 0)"/>
<use xlink:href="#DejaVuSans-Bold-51" transform="translate(937.21875 0)"/> <use xlink:href="#DejaVuSans-Bold-51" transform="translate(937.21875 0)"/>
@@ -1957,9 +1927,67 @@ z
</g> </g>
</g> </g>
<g id="text_11"> <g id="text_11">
<!-- 0.89 s · 2,500 matched --> <!-- 2.18 s · 4,591 entities/s -->
<g style="fill: #e6edf3" transform="translate(455.643746 132.262117) scale(0.09 -0.09)"> <g style="fill: #e6edf3" transform="translate(522.123507 132.262117) scale(0.09 -0.09)">
<defs> <defs>
<path id="DejaVuSans-Bold-1b" d="M 2228 2088
Q 1891 2088 1709 1903
Q 1528 1719 1528 1375
Q 1528 1031 1709 848
Q 1891 666 2228 666
Q 2563 666 2741 848
Q 2919 1031 2919 1375
Q 2919 1722 2741 1905
Q 2563 2088 2228 2088
z
M 1350 2484
Q 925 2613 709 2878
Q 494 3144 494 3541
Q 494 4131 934 4440
Q 1375 4750 2228 4750
Q 3075 4750 3515 4442
Q 3956 4134 3956 3541
Q 3956 3144 3739 2878
Q 3522 2613 3097 2484
Q 3572 2353 3814 2058
Q 4056 1763 4056 1313
Q 4056 619 3595 264
Q 3134 -91 2228 -91
Q 1319 -91 855 264
Q 391 619 391 1313
Q 391 1763 633 2058
Q 875 2353 1350 2484
z
M 1631 3419
Q 1631 3141 1786 2991
Q 1941 2841 2228 2841
Q 2509 2841 2662 2991
Q 2816 3141 2816 3419
Q 2816 3697 2662 3845
Q 2509 3994 2228 3994
Q 1941 3994 1786 3844
Q 1631 3694 1631 3419
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-17" d="M 2356 3675
L 1038 1722
L 2356 1722
L 2356 3675
z
M 2156 4666
L 3494 4666
L 3494 1722
L 4159 1722
L 4159 850
L 3494 850
L 3494 0
L 2356 0
L 2356 850
L 288 850
L 288 1881
L 2156 4666
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-1c" d="M 641 103 <path id="DejaVuSans-Bold-1c" d="M 641 103
L 641 966 L 641 966
Q 928 831 1190 764 Q 928 831 1190 764
@@ -1990,31 +2018,40 @@ Q 1491 2759 1650 2554
Q 1809 2350 2125 2350 Q 1809 2350 2125 2350
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
<path id="DejaVuSans-Bold-18" d="M 678 4666 </defs>
L 3669 4666 <use xlink:href="#DejaVuSans-Bold-15"/>
L 3669 3781 <use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/>
L 1638 3781 <use xlink:href="#DejaVuSans-Bold-14" transform="translate(107.5625 0)"/>
L 1638 3059 <use xlink:href="#DejaVuSans-Bold-1b" transform="translate(177.140625 0)"/>
Q 1775 3097 1914 3117 <use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/>
Q 2053 3138 2203 3138 <use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/>
Q 3056 3138 3531 2711 <use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/>
Q 4006 2284 4006 1522 <use xlink:href="#DejaVuSans-Bold-3" transform="translate(375.859375 0)"/>
Q 4006 766 3489 337 <use xlink:href="#DejaVuSans-Bold-79" transform="translate(410.671875 0)"/>
Q 2972 -91 2053 -91 <use xlink:href="#DejaVuSans-Bold-3" transform="translate(448.65625 0)"/>
Q 1656 -91 1267 -14 <use xlink:href="#DejaVuSans-Bold-3" transform="translate(483.46875 0)"/>
Q 878 63 494 219 <use xlink:href="#DejaVuSans-Bold-17" transform="translate(518.28125 0)"/>
L 494 1166 <use xlink:href="#DejaVuSans-Bold-f" transform="translate(587.859375 0)"/>
Q 875 947 1217 837 <use xlink:href="#DejaVuSans-Bold-18" transform="translate(625.84375 0)"/>
Q 1559 728 1863 728 <use xlink:href="#DejaVuSans-Bold-1c" transform="translate(695.421875 0)"/>
Q 2300 728 2551 942 <use xlink:href="#DejaVuSans-Bold-14" transform="translate(765 0)"/>
Q 2803 1156 2803 1522 <use xlink:href="#DejaVuSans-Bold-3" transform="translate(834.578125 0)"/>
Q 2803 1891 2551 2103 <use xlink:href="#DejaVuSans-Bold-48" transform="translate(869.390625 0)"/>
Q 2300 2316 1863 2316 <use xlink:href="#DejaVuSans-Bold-51" transform="translate(937.21875 0)"/>
Q 1603 2316 1309 2248 <use xlink:href="#DejaVuSans-Bold-57" transform="translate(1008.40625 0)"/>
Q 1016 2181 678 2041 <use xlink:href="#DejaVuSans-Bold-4c" transform="translate(1056.203125 0)"/>
L 678 4666 <use xlink:href="#DejaVuSans-Bold-57" transform="translate(1090.484375 0)"/>
z <use xlink:href="#DejaVuSans-Bold-4c" transform="translate(1138.28125 0)"/>
" transform="scale(0.015625)"/> <use xlink:href="#DejaVuSans-Bold-48" transform="translate(1172.5625 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(1240.390625 0)"/>
<use xlink:href="#DejaVuSans-Bold-12" transform="translate(1299.90625 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(1336.4375 0)"/>
</g>
</g>
<g id="text_12">
<!-- 2.47 s · 2,500 matched -->
<g style="fill: #e6edf3" transform="translate(535.694643 74.23186) scale(0.09 -0.09)">
<defs>
<path id="DejaVuSans-Bold-50" d="M 3781 2919 <path id="DejaVuSans-Bold-50" d="M 3781 2919
Q 3994 3244 4286 3414 Q 3994 3244 4286 3414
Q 4578 3584 4928 3584 Q 4578 3584 4928 3584
@@ -2150,10 +2187,10 @@ Q 1825 722 2181 722
z z
" transform="scale(0.015625)"/> " transform="scale(0.015625)"/>
</defs> </defs>
<use xlink:href="#DejaVuSans-Bold-13"/> <use xlink:href="#DejaVuSans-Bold-15"/>
<use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/> <use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/>
<use xlink:href="#DejaVuSans-Bold-1b" transform="translate(107.5625 0)"/> <use xlink:href="#DejaVuSans-Bold-17" transform="translate(107.5625 0)"/>
<use xlink:href="#DejaVuSans-Bold-1c" transform="translate(177.140625 0)"/> <use xlink:href="#DejaVuSans-Bold-1a" transform="translate(177.140625 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/> <use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/> <use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/>
@@ -2176,38 +2213,6 @@ z
<use xlink:href="#DejaVuSans-Bold-47" transform="translate(1287.171875 0)"/> <use xlink:href="#DejaVuSans-Bold-47" transform="translate(1287.171875 0)"/>
</g> </g>
</g> </g>
<g id="text_12">
<!-- 2.16 s · 4,638 entities/s -->
<g style="fill: #e6edf3" transform="translate(546.98514 74.23186) scale(0.09 -0.09)">
<use xlink:href="#DejaVuSans-Bold-15"/>
<use xlink:href="#DejaVuSans-Bold-11" transform="translate(69.578125 0)"/>
<use xlink:href="#DejaVuSans-Bold-14" transform="translate(107.5625 0)"/>
<use xlink:href="#DejaVuSans-Bold-19" transform="translate(177.140625 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(246.71875 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(281.53125 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(341.046875 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(375.859375 0)"/>
<use xlink:href="#DejaVuSans-Bold-79" transform="translate(410.671875 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(448.65625 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(483.46875 0)"/>
<use xlink:href="#DejaVuSans-Bold-17" transform="translate(518.28125 0)"/>
<use xlink:href="#DejaVuSans-Bold-f" transform="translate(587.859375 0)"/>
<use xlink:href="#DejaVuSans-Bold-19" transform="translate(625.84375 0)"/>
<use xlink:href="#DejaVuSans-Bold-16" transform="translate(695.421875 0)"/>
<use xlink:href="#DejaVuSans-Bold-1b" transform="translate(765 0)"/>
<use xlink:href="#DejaVuSans-Bold-3" transform="translate(834.578125 0)"/>
<use xlink:href="#DejaVuSans-Bold-48" transform="translate(869.390625 0)"/>
<use xlink:href="#DejaVuSans-Bold-51" transform="translate(937.21875 0)"/>
<use xlink:href="#DejaVuSans-Bold-57" transform="translate(1008.40625 0)"/>
<use xlink:href="#DejaVuSans-Bold-4c" transform="translate(1056.203125 0)"/>
<use xlink:href="#DejaVuSans-Bold-57" transform="translate(1090.484375 0)"/>
<use xlink:href="#DejaVuSans-Bold-4c" transform="translate(1138.28125 0)"/>
<use xlink:href="#DejaVuSans-Bold-48" transform="translate(1172.5625 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(1240.390625 0)"/>
<use xlink:href="#DejaVuSans-Bold-12" transform="translate(1299.90625 0)"/>
<use xlink:href="#DejaVuSans-Bold-56" transform="translate(1336.4375 0)"/>
</g>
</g>
<g id="text_13"> <g id="text_13">
<!-- Self-measurement only: competitor servers would pay extra stdio serialization cost in-process runs do not. Reproduce: python -m benchmarks.perf_suite --> <!-- Self-measurement only: competitor servers would pay extra stdio serialization cost in-process runs do not. Reproduce: python -m benchmarks.perf_suite -->
<g style="fill: #8b949e" transform="translate(263.723678 328.046136) scale(0.09 -0.09)"> <g style="fill: #8b949e" transform="translate(263.723678 328.046136) scale(0.09 -0.09)">

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB