Skip to content

perf: optimize Python and Rust serializer hot paths#340

Merged
vinitkumar merged 12 commits into
masterfrom
perf/flamegraph-python-315
Jul 15, 2026
Merged

perf: optimize Python and Rust serializer hot paths#340
vinitkumar merged 12 commits into
masterfrom
perf/flamegraph-python-315

Conversation

@vinitkumar

@vinitkumar vinitkumar commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • fast-path exact JSON-native types in the pure-Python serializer while preserving compatibility fallbacks
  • replace the Rust serializer's scalar XML escape scan with a sparse-fast, dense-linear memchr strategy
  • retain the Rust 16 KiB streaming bytes writer and its documented memory bound
  • confirm 16 KiB remains the best output-buffer capacity in a 4–128 KiB sweep
  • add symbolized before/after flamegraphs for both implementations

All profiling and benchmarks used uv-managed CPython 3.15.0b3.

Pure Python results

Deterministic 5,000-record nested payload:

Metric Before After Change
timeit conversion 83.0 ms 57.2 ms 31.1% lower
20-loop traced time 8.311 s 5.782 s 30.4% lower
function calls 48.17M 30.13M 37.4% fewer
isinstance calls 11.70M 2.80M 76.1% fewer
Python before Python after
Python before flamegraph Python after flamegraph

Rust results

The symbolized native profile showed the scalar XML escape loop consuming 14.31% of exclusive samples. Hybrid memchr scanning reduced that to 7.97% while preserving all five substitutions and UTF-8 boundaries. After four sparse matches it switches to monotonic scanners, keeping dense inputs linear.

Paired release benchmark: 21 rounds × 50 conversions of the same 5,000-record payload.

Metric Before After Change
Median conversion 6.007 ms 5.632 ms 6.23% lower
Mean conversion 6.013 ms 5.643 ms 6.14% lower
Escape scanner exclusive samples 14.31% 7.97% 44.3% lower share

The 100,000-record memory benchmark remains at an 80.19 MiB serializer RSS delta for a 78.17 MiB output.

A post-optimization capacity sweep tested 4, 8, 16, 32, 64, and 128 KiB. An ABBA-interleaved confirmation measured 16 KiB at 5.974 ms versus 6.024 ms for 32 KiB, so the existing capacity remains the best measured choice.

Rust before Rust after
Rust before flamegraph Rust after flamegraph

The Rust profiles were captured with Samply's macOS native sampler and rendered as SVG flamegraphs with Inferno.

Validation

  • 421 passed on CPython 3.15.0b3 with the Rust extension loaded and exactly 100% statement coverage
  • cargo test: 48 passed
  • cargo clippy --all-targets -- -D warnings
  • cargo fmt --check
  • ruff check json2xml tests
  • ty check json2xml tests
  • lat check

@sourcery-ai

sourcery-ai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Optimizes the pure-Python JSON-to-XML serializer by fast-pathing native JSON scalar/container types, reducing abstract isinstance dispatch, tightening XML escape/attr handling, and documenting CPython 3.15 performance and flamegraph results alongside matching tests and release metadata updates.

File-Level Changes

Change Details Files
Introduce a dedicated numeric classifier and reorder type checks to fast-path native JSON scalars and containers in serializer helpers.
  • Add _is_number helper that treats built-in ints/floats/complex as numbers while delegating uncommon cases to numbers.Number
  • Update get_xml_type, get_xpath31_tag_name, and is_primitive_type to use concrete type checks for dict/list/tuple/str/bool and the new _is_number helper
  • Refactor hot-path helpers (_append_convert, _append_rawitem, _append_convert_dict, _append_convert_list) to branch on exact types (bool, str, int, float, complex, dict, list, tuple) before falling back to generic isinstance checks
json2xml/dicttoxml.py
Optimize XML escaping and attribute handling to avoid unnecessary set allocations and redundant validation.
  • Change escape_xml to use _XML_ESCAPE_CHARS.isdisjoint instead of intersection to detect unescaped strings without allocating temporary sets
  • Adjust make_attrstring to skip validate_xml_attr_names when attr is empty and to validate only once for the single-attr fast path
json2xml/dicttoxml.py
Extend tests and LAT documentation to pin the new numeric fast-path behavior and preserve general Number support.
  • Add test_number_classifier_preserves_supported_number_types to validate _is_number behavior for built-in numeric types, Decimal, Fraction, complex, custom Number, bool, and non-numeric strings
  • Document the numeric fast-path contract in LAT tests documentation under "Numeric fast path preserves general Number support"
tests/test_dicttoxml_unit.py
lat.md/tests.md
Document CPython 3.15 flamegraph-driven optimizations and add flamegraph artifacts.
  • Update architecture documentation with CPython 3.15.0b3 flamegraph findings, call-count and timing improvements, and the role of _append_convert_dict and _is_number in the optimization
  • Add docs/flamegraphs README summarizing the benchmark setup and before/after profiling metrics
  • Check in before/after CPython 3.15 SVG flamegraph artifacts
lat.md/architecture.md
docs/flamegraphs/README.md
docs/flamegraphs/python315-before.svg
docs/flamegraphs/python315-after.svg

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vinitkumar vinitkumar marked this pull request as ready for review July 15, 2026 16:38
@vinitkumar vinitkumar force-pushed the perf/flamegraph-python-315 branch from 40c9128 to d689599 Compare July 15, 2026 16:39

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="json2xml/dicttoxml.py" line_range="517-526" />
<code_context>
+    elif obj_type is int or obj_type is float or obj_type is complex:
</code_context>
<issue_to_address>
**suggestion:** Numeric handling in `_append_convert` is split between exact-type checks and `_is_number`, which complicates the control flow and subclass behavior.

The current ordering treats exact `int`/`float`/`complex` via `type(obj) is ...`, while `Decimal` and other `numbers.Number` subclasses are only caught later via `_is_number`. Likewise, exact `dict`/`list`/`tuple` are handled before `_is_number`, while subclasses rely on later `isinstance` checks. This works but makes subclass behavior dependent on the precise ordering of `type(...) is ...` vs `isinstance(...)` branches. Consider either routing all numerics through `_is_number` or adding a brief comment explaining this intentional split between exact types and subclasses to prevent future regressions.

Suggested implementation:

```python
    if obj_type is bool:
        output.write(convert_bool(key=item_name, val=obj, attr_type=attr_type, cdata=cdata))
    elif obj_type is str:
        output.write(convert_kv(key=item_name, val=obj, attr_type=attr_type, attr={}, cdata=cdata))
    elif obj is None:

```

This change assumes that a later branch in `_append_convert` (or the surrounding logic) already handles all numeric values via `_is_number(obj)` and converts them appropriately. If such a branch does not yet exist, you should add an `elif _is_number(obj): ...` clause to preserve the previous behavior for `int`/`float`/`complex` and any other `numbers.Number` subclasses.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread json2xml/dicttoxml.py
@vinitkumar vinitkumar changed the title perf: fast-path native JSON serializer types perf: optimize Python and Rust serializer hot paths Jul 15, 2026
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4a6903a) to head (8946690).

Additional details and impacted files
@@            Coverage Diff            @@
##            master      #340   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            7         7           
  Lines          714       759   +45     
=========================================
+ Hits           714       759   +45     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Exercise every exact-type compatibility fallback, restore string-subclass type metadata, and enforce 100% Python statement coverage.
Document the measured XML escape-scan improvement, gate publication on built-wheel tests, cover Python 3.15 beta, and isolate Rust releases from the Python publisher.
@vinitkumar vinitkumar merged commit 560f743 into master Jul 15, 2026
65 checks passed
@vinitkumar vinitkumar deleted the perf/flamegraph-python-315 branch July 15, 2026 19:00
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