perf(pypangraph): decode graphs into typed msgspec models - #202
Draft
ivan-aksamentov wants to merge 3 commits into
Draft
perf(pypangraph): decode graphs into typed msgspec models#202ivan-aksamentov wants to merge 3 commits into
ivan-aksamentov wants to merge 3 commits into
Conversation
Schema validation dominates the cost of loading a graph: on a mid-sized graph the pure-Python jsonschema pass takes seconds while parsing takes tens of milliseconds, because the validator walks every node, edit and position in interpreted Python. Decode the JSON bytes directly into the typed model in model.py with msgspec. Parsing and validation happen together in compiled code, with no intermediate dict tree, so the load is bounded by decompression rather than validation. The models carry the schema's constraints (non-negative integers, single-character alt, the strand enum), and the path, block and node collections now read the typed structs. Pangraph still accepts a plain dict; it is validated and converted with msgspec.convert, so building a graph from an in-memory dict enforces the same schema as loading from a file.
Lock in which malformed graphs the loader rejects (missing required fields, wrong types, out-of-range values, bad strand enum), so the accept/reject contract holds independently of the validation engine.
Document how a graph is read, decoded into typed models and constructed, and why loading uses msgspec. Add a benchmark that times each load phase and every validation engine present, so the numbers can be reproduced on one machine.
This was referenced Aug 18, 2026
ivan-aksamentov
marked this pull request as draft
August 18, 2026 12:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Alternative PRs (mutually exclusive, merge one):
Problem
Pangraph.from_jsonvalidates every loaded graph against the JSON schema generated from the Rust types before building the object. On real graphs that validation, not parsing, dominates load time. Issue #200 reports the slowdown.On the benchmark graph below, parsing the JSON takes about 40 ms while the pure-Python
jsonschemapass takes about 2 seconds: the validator walks every node, edit and position in interpreted Python.This change
Decode the JSON bytes directly into the typed model in
pypangraph/model.pywithmsgspec. Parsing and validation happen together in compiled code, with no intermediate dict tree, so the load is bounded by decompression rather than validation. The models carry the schema's constraints (non-negative integers, single-characteralt, thestrandenum), and the path, block and node collections read the typed structs.Pangraphstill accepts a plain dict, which is validated and converted withmsgspec.convert, so an in-memory dict enforces the same schema as a file.Why this is the one to merge
Pangraphand its collections is unchanged.If the goal is the fastest load and better-typed internals, this is the strongest choice.
Benchmark
Fixture:
packages/pypangraph/tests/data/staph.json.gz(664 blocks, 6817 nodes, 15 paths; 1.81 MB compressed, 9.72 MB decoded). Median of 7 runs on one machine in the project Python container, measured bypackages/pypangraph/benchmarks/bench_load. Correctness parity was verified for every engine: each accepts the valid graph and rejects missing-field, wrong-type, negative-value and bad-strand mutations.Full load (parse and validate together):
json+jsonschema)Per-phase breakdown. Decompression is shared; this PR replaces JSON parsing and validation with a single typed decode.
Shared (unchanged by this PR):
Parse and validate (what this PR changes):
Methodology notes:
jsonschemavalidator does not help; the cost is the interpreted traversal, not validator construction.fastjsonschemawas rejected: it errors on the schema'sformat: uintannotation.format: uintis a decorative annotation; the non-negative range is enforced byminimum: 0. No engine asserts on the format string.Conclusion: validation is about 98% of load time, and every candidate removes it.
msgspecis the fastest because it folds parsing and validation into a single compiled decode that emits typed structs, so the load is bounded by decompression rather than validation.The three alternatives
All three PRs branch from
feat/mergeand rewrite the same loader; they cannot be combined.model_validate_json. Same typed internals from the most widely used validation library, about 22x, slower than msgspec.Pick msgspec for the fastest load with typed internals; pick jsonschema-rs for the smallest diff, or pydantic to lean on the more common ecosystem.
Work items
msgspecmodels mirroring the schema inpypangraph/model.py.pypangraph/class_graph.py; accept a dict viamsgspec.convert.msgspecinpyproject.toml,requirements.txt, and the Python container.benchmarks/bench_loadand a graph-loading doc.Possible improvements
datamodel-codegen(as the Makefile already does for the dataclass example) so they cannot drift from the Rust types.Verify