Skip to content

perf: send query responses up to 3x faster by reusing mapped ASTs and their JSON - #43

Merged
jnsdls merged 1 commit into
mainfrom
perf/query-response-text
Oct 28, 2026
Merged

jnsdls merged 1 commit into
mainfrom
perf/query-response-text

Conversation

@jnsdls

@jnsdls jnsdls commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up to #39/#40. Upstream 1.11.3 normalizes ASTs at every builder step, which made the query path slower. This wins it back on the query endpoint, mostly by not doing the same work twice for a query the builder already interned.

 handle_query_request
   handler(name, args)            # interned query, often the same object as last time
-  map_ast(query.ast, mapper)
+  mapped_ast(query, mapper)      # kept on the Query, keyed by mapper identity
 starlette /query
-  js_stringify_bytes(response.to_json())          # orjson, then _may_differ scans ~200 KB
+  query_response_bytes(response)
+    QuerySuccess.to_json_bytes()
+      TransformedQuery.to_json_bytes()
+        AST.to_json_bytes()      # js_stringify_bytes(to_json()) once, kept on the AST
 js_stringify_bytes._may_differ
-  10 substring scans, one per digit, for an array-index key
+  digits deleted with translate, one scan for '"":'
 builder
-  _derive(("related_base"/"exists_base", rel), lambda: ...)
+  _find, then _keep only on a miss (no closure per call)
+  _condition_key: column-vs-scalar condition keyed as one flat tuple

ASTs are already immutable by contract (ast.py: "no code changes a node once built"), so caching their mapped form and their text is safe. AST._text is a compare=False, init=False slot and is reset in _copy.

Evidence

Wall clock, python -m bench, same machine, this branch against main (ca665da):

case main this PR Py/TS main Py/TS now
query.simple.b1 6.9 µs 5.2 µs 1.71 1.26
query.simple.b100 541.5 µs 388.0 µs 2.96 2.15
query.complex.b1 31.9 µs 20.3 µs 3.12 1.97
query.complex.b100 2.86 ms 1.81 ms 3.94 2.33
query_json.simple.b1 15.4 µs 8.0 µs 2.56 1.34
query_json.simple.b100 1.05 ms 543.8 µs 3.53 1.64
query_json.complex.b1 68.8 µs 23.5 µs 3.82 1.31
query_json.complex.b100 6.14 ms 2.05 ms 4.63 1.57
build.complex 14.6 µs 13.7 µs 6.54 5.68
mutate_cpu.put.n1 16.3 µs 14.6 µs 4.63 3.84
parse, parse_json, map, z2s unchanged (noise)

query_json now times query_response_bytes, which is what the Starlette route sends.

  • New differential assertion in test_queries_match_upstream: query_response_bytes(actual) == JSON.stringify(upstream response), byte for byte, over the Hypothesis program corpus (floats, surrogates, errors, userID set/unset).
  • uv run pytest: 784 passed, 100% coverage. python -m tests.compat: 54 upstream modules, every port module fully exercised. ruff, ty clean.

The caches pay off when a query repeats (same name, same args), which interning already makes the common case. A query with args that vary per request still maps and stringifies once per distinct query.

Merge Danger

Door: two-way

Blast Radius: query endpoint

Response bytes are unchanged (proven against upstream). Mapped ASTs are now shared between responses for the same interned query, so code that mutates a returned AST would corrupt later responses. Nothing in the SDK does that.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

… their JSON

- An interned query keeps its server-named AST, so a query built again maps once.
- An AST keeps its JSON text; the query endpoint joins each query's text instead of
  stringifying and scanning the whole response.
- js_stringify_bytes finds array-index keys in one scan instead of ten.
- The builder looks up a relationship's base query without a closure, and keys a
  column-to-scalar condition as one flat tuple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codspeed

codspeed Bot commented Oct 28, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 48.68%

1 warning

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

⚡ 9 improved benchmarks
✅ 37 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ test_cpu[query_json.complex.b100] 127.2 ms 58.1 ms ×2.2
⚡ test_cpu[query_json.complex.b1] 1,666.7 µs 879.5 µs +89.5%
⚡ test_cpu[query_json.simple.b100] 25.5 ms 16.2 ms +57.14%
⚡ test_cpu[query.complex.b100] 72.9 ms 49.7 ms +46.67%
⚡ test_cpu[query_json.simple.b1] 611.2 µs 438.3 µs +39.46%
⚡ test_cpu[query.simple.b100] 15.2 ms 11.1 ms +37.14%
⚡ test_cpu[query.complex.b1] 1,009.6 µs 763.9 µs +32.17%
⚡ test_cpu[parse_json.transform.q1000] 67.2 ms 55.1 ms +22.13%
⚡ test_cpu[query.simple.b1] 408.9 µs 339.7 µs +20.38%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/query-response-text (76c471d) with main (ca665da)

Open in CodSpeed

@jnsdls
jnsdls merged commit 1f4dd7f into main Oct 28, 2026
9 checks passed
@jnsdls
jnsdls deleted the perf/query-response-text branch October 28, 2026 21:42
jnsdls added a commit that referenced this pull request Oct 28, 2026
…benchmarks (#46)

## Summary

The docs had fallen behind #39 (1.11.3), #40 (interning) and #43 (cached
mapped ASTs). This brings them level and adds current benchmark ratios.

```diff
 README.md
+  "Queries are shared": interned queries, mapped AST and JSON reused; treat as immutable
-  dangling "wire and SQL tiers run against every adapter" line
+  **Adapters.** bullet in the compatibility list
+  build/map in the CPU case list
+  Py/TS ratio table at ec0768a, 1.11.3, Apple Silicon laptop
 docs/compat.md
+  known difference: logging (stdlib `logging`, not upstream's LogSink)
   re-wrap the interned-queries paragraph
 docs/plan.md
-  "small bun runner"            -> Node runner in harness/
-  "first pin is 1.10.0"         -> was 1.10.0; current pin in ZERO_VERSION
+  status: phases 1-7 done; phase 8 publishes on tag, npm watch not built
 CONTRIBUTING.md "Bumping Zero"
+  README version, docstring tag/commit, compat_scope line remap,
+  `python -m bench.capture`, record new differences
 bench/__init__.py
+  build and map groups
```

## Evidence

Nothing on screen changes; docs only.

- Every API name the README names is exported from `zero_server`. The
version is 1.11.3 everywhere it's pinned (`ZERO_VERSION`, both
`package.json` files, the compose image, the docstrings).
- The ratio table comes from a full local `uv run python -m bench` (exit
0) at ec0768a. Postgres cases: Python within 10% of npm or faster. Query
endpoint with JSON: 1.3–1.7x. `build`/`map`: 6–14x, but microseconds per
call.
- `ruff check` and `ruff format --check` pass on `bench/`.

## Merge Danger

**Door:** two-way

**Blast Radius:** docs

README is the PyPI long description, so the next release's page shows
the new table. The ratios are from one laptop and will drift; the table
names the commit and the machine.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/agent-labs-dev/codesmith/zero-python/pr/46?autoLogin=true&ref=codesmith_pr_footer"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v3.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png"
width="162" height="28"></picture></a> <a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1795816999&installation_model_id=434381&pr_number=46&ref=codesmith_pr_footer&repository=agent-labs-dev%2Fzero-python&return_to=https%3A%2F%2Fgithub.com%2Fagent-labs-dev%2Fzero-python%2Fpull%2F46&signature=c4188e5be40817be60bd25370789884b1853df2c8efe7ef6c8707f41d6f4f79f"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png"
width="190" height="28"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@jnsdls jnsdls mentioned this pull request Oct 28, 2026
jnsdls added a commit that referenced this pull request Oct 28, 2026
Bumps the version to 0.0.4 in `pyproject.toml`, `uv.lock` and
`zero_server.__version__`. Nothing else changes.

Since 0.0.3, `main` has gained support for `@rocicorp/zero` 1.11.3
(#39), orjson for request parsing and response writing (#37), query
reuse that builds queries up to 2.7x faster (#40), reused mapped ASTs
and JSON that send query responses up to 3x faster (#43), updated docs
(#46) and dependency bumps.

After merge, tag `v0.0.4` on main to publish to PyPI.

Local run: 786 passed, 100% coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/agent-labs-dev/codesmith/zero-python/pr/47?autoLogin=true&ref=codesmith_pr_footer"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v3.svg"><img
alt="View with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png"
width="162" height="28"></picture></a> <a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1795817623&installation_model_id=434381&pr_number=47&ref=codesmith_pr_footer&repository=agent-labs-dev%2Fzero-python&return_to=https%3A%2F%2Fgithub.com%2Fagent-labs-dev%2Fzero-python%2Fpull%2F47&signature=bf55280b5bd9ccc8d24e9874ca54b3b0973db3ee2b748438e15b920b024f233f"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with [code]smith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png"
width="190" height="28"></picture></a>
<sup>Need help on this PR? Tag <code>@codesmith-bot</code> with what you
need.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant