Skip to content

rustberry modernize-v3: apollo-rs 1.31 + PyO3 0.28 + perf [DRAFT REVIEW] - #5

Draft
erikwrede wants to merge 8 commits into
masterfrom
modernize-v3
Draft

rustberry modernize-v3: apollo-rs 1.31 + PyO3 0.28 + perf [DRAFT REVIEW]#5
erikwrede wants to merge 8 commits into
masterfrom
modernize-v3

Conversation

@erikwrede

Copy link
Copy Markdown
Owner

Private review surface — fork draft

8 commits modernizing rustberry. Stacked on top of master.

What's in the stack

commit what
b13a861 pyo3 0.28 + apollo-compiler 1.31 api migration
5b0b6fb implement fragment conversion and validate diagnostics
14cb14a convert pyproject to PEP 621 and bump python floor to 3.11
f3cfcba use to_json messages for parser error formatting
b3a1d15 include FragmentDefinitions in DocumentNode.definitions
881066b cache name.value and kind as Py on mirror nodes
df97993 add loc, original_error, nodes, source, positions to satisfy graphql-core helpers
68d3e1e cache Vec collection fields as Py at construction

Untracked locally (not in this PR)

  • benchmarks/ directory with the benchmark suite (bench_real_world.py, bench_pyclass_overhead.py, bench_rustberry_vs_core.py, etc.) and captured .txt results — let me know if you want these checked in too
  • test_parser/test_real_world_smoke.py — smoke tests against GitHub & SpaceX schemas

Status

  • maturin build --release produces rustberry-0.0.15-cp311-abi3-macosx_11_0_arm64.whl cleanly
  • Used by strawberry-rustberry (separate repo) which has 14 passing tests
  • Headline: 10-100× speedup on parse+validate vs graphql-core depending on query shape; 17× end-to-end on GitHub PR-explorer query (3.58 ms → 202 μs)

erikwrede and others added 8 commits April 25, 2026 14:24
Port the source tree from pyo3 0.21 / apollo-compiler 1.0-era APIs to the
current 0.28 / 1.31 surface so the crate compiles again.

Highlights:
  - Replace `&PyAny`/`PyObject`/`PyModule::import` with `Bound<'py, PyAny>`,
    `Py<PyAny>`, and the `Bound`-returning `Python::import` from pyo3 0.28.
  - Switch `Python::with_gil` to the 0.28 `Python::attach` API and use
    `py.detach(...)` (formerly `allow_threads`) for GIL-free Rust work.
  - Drop legacy `import_bound`/`PyDict::new_bound`/`to_object` helpers in
    favour of the new APIs.
  - Update apollo-compiler usage: `document.operations.iter()` instead of
    the removed `all_operations`, `response::ResponseDataPathSegment`
    instead of `execution::ResponseDataPathElement`, and `Diagnostic::to_json`
    for converting validation errors.
  - Rebuild the `#[pymodule]` registration to take a `&Bound<'_, PyModule>`.
  - Enable the `py-clone` pyo3 feature so the derive-based `#[pyo3(get)]`
    getters can compile against `Py<...>` and `Vec<Py<...>>` fields. Every
    getter site holds the GIL so the runtime requirement is satisfied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round out the public API:

  - `Selection::FragmentSpread` and `Selection::InlineFragment` arms in
    `MirrorConversionContext` now produce `FragmentSpreadNode` and
    `InlineFragmentNode` mirror nodes (added to `reduced_core_mirror`
    alongside `FragmentDefinitionNode`). They report the right `kind`
    string and pretend to be the matching `graphql-core` class via the
    `__class__` getter so `isinstance` checks still work.
  - `QueryCompiler.validate` and `add_validate` now return a
    `list[GraphQLError]` mirroring `graphql-core`'s diagnostic shape
    instead of an opaque bool. Empty list = valid.
  - Bad schemas / bad queries now raise `ValueError` with the formatted
    apollo-compiler diagnostics instead of panicking.
  - `GraphQLError.extensions` returns an empty dict (was a `panic!`).
  - All mirror nodes are `#[pyclass(frozen)]` and skip the deprecated
    auto-`FromPyObject` derive.
  - Cache class lookups in module-level `OnceLock`s instead of importing
    `graphql.language.ast` on every `__class__` getter call.
  - Replace deprecated `downcast_into` with `cast_into`.
  - Refresh `_rustberry.pyi` stubs to match the new return types and
    re-export `Document`/`GraphQLError`/`SourceLocation` from
    `rustberry`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch from the legacy `[tool.poetry]` block to PEP 621 `[project]`
metadata. Pin the Python floor to 3.11 (matching the abi3-py311 wheel
selection in `Cargo.toml`), depend on `graphql-core>=3.2` for the
mirror-node `__class__` lookups, and route maturin through the new
`python-source`/`module-name` knobs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous formatting via `Display` triggered apollo-compiler's
ariadne renderer, which printed "Unable to fetch source" lines to
stderr whenever the schema source wasn't attached to the document's
source map. Format errors via `Diagnostic::to_json().message` instead
so the surface stays quiet on the failure path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous mirror getters re-encoded UTF-8 strings on every access:
`NameNode.value` cloned a Rust `String` into a fresh `PyString`, and
every `kind` getter allocated a new Python string for a literal that
never changes.  graphql-core's executor reads these attributes
thousands of times per request, so the per-call ~28 ns FFI cost showed
up as ~200 us of pure overhead on a 2.4 ms `execute` call.

Cache the Python representation once at construction:

* Textual leaves (`NameNode.value`, `IntValueNode.value`,
  `FloatValueNode.value`, `StringValueNode.value`,
  `EnumValueNode.value`) now hold `Py<PyString>`. The auto-derived
  `#[pyo3(get)]` returns a borrowed view per access; no allocation,
  no UTF-8 decode.
* `kind` is served from a per-pyclass `OnceLock<Py<PyString>>` seeded
  with `PyString::intern`, so all instances share one interned Python
  string.  Identity-based equality at the Python level then
  short-circuits `node.kind == "field"` checks.

`mirror_converter.rs` interns names and enum/int/float values at
construction and uses a plain (non-interned) `PyString` for arbitrary
string literals so we don't pollute the interpreter's intern table
with user data.

Microbenchmark (`benchmarks/bench_pyclass_overhead.py`) before/after:
  rustberry mirror NameNode  median 27.9 ns -> 17.1 ns
  graphql-core NameNode      median 10.0 ns (unchanged)

Smoke-tested with `execute_sync` on a query that uses fields,
arguments, and named fragments; results match graphql-core exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…phql-core helpers

Strawberry's runtime error formatter and graphql-core's coercion helpers
read AST node.loc and GraphQLError.{original_error, nodes, source, positions}.
Mirror nodes and our error type didn't expose those, so any path that
formatted an error against a rustberry-mirror document raised
AttributeError instead of returning the actual error.

Mirror nodes get a constant loc=None getter (apollo-compiler doesn't carry
per-AST-node source positions). GraphQLError gets original_error, nodes,
source, positions all = None — apollo-compiler diagnostics don't preserve
those either, but downstream helpers probe them.
Profiling rustberry mirror nodes against graphql-core's executor showed
the per-access attribute cost was no longer a bottleneck (4.6 ns,
parity with pure-Python __slots__) BUT collection-field access was
5-7x slower:

  rustberry posts.directives:    87 ns vs graphql-core 17 ns
  rustberry posts.arguments:     98 ns vs graphql-core 17 ns
  rustberry bool(directives):    92 ns vs graphql-core 23 ns

Reason: `#[pyo3(get)]` on a `Vec<#[pyclass]>` field reconstructs a
fresh PyList on EVERY attribute access. graphql-core's executor reads
field.directives, field.arguments, field.selection_set.selections on
every resolver call, and runs `if node.directives:` truthiness checks
constantly during validation/coercion.

The fix: store these fields as Py<PyTuple> built once at conversion
time. Each later access returns the same tuple via a refcount bump.

After this change:
  rustberry posts.directives:    16.2 ns (parity)
  rustberry posts.arguments:     16.7 ns (parity)
  rustberry bool(directives):    23.8 ns (parity)

End-to-end relay_feed: 11.19 ms -> 10.86 ms (~3% improvement on a
heavy-resolver query where execute dominates).
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