Skip to content

perf(TypeTreeNode): parse_blob -- 2.6x faster warm test_save - #396

Open
dexhunter wants to merge 1 commit into
K0lb3:masterfrom
dexhunter:perf/typetree-parse-blob-v2
Open

dexhunter wants to merge 1 commit into
K0lb3:masterfrom
dexhunter:perf/typetree-parse-blob-v2

Conversation

@dexhunter

Copy link
Copy Markdown

TL;DR

  • Reuse cleaned node names within each type-tree blob and construct ordinary nodes positionally.
  • Optimize TypeTreeNode.parse_blob on the repository's tests/test_main.py::test_save load/save/reload workload.
  • Measure warmed wall time with standard-library timeit: median of five repeats of 30 calls, after an untimed warm-up, on an isolated core.
  • Before -> after: 91.7 ms -> 35.0 ms per warmed test_save call (2.6x faster). Three frozen-harness candidate/baseline ratios were 0.414, 0.397, and 0.425.
  • Validation: 44 native tests passed, full Ruff lint/format checks passed, and 400 supplemental differential cases matched the baseline.

Problem and change

parse_blob constructs a node for every binary type-tree entry. The constructor derives _clean_name each time; the compiled implementation constructs two regular expressions even for names that remain unchanged. Repeated names therefore repeat expensive work.

This change keeps a clean-name cache local to each parse. Plain ASCII identifiers other than pass and from already have their final clean name. Other names use the first real node's computed value. When that value is available, the node uses the inexpensive empty-name constructor path and then receives its original name and cached clean name. Ordinary nodes use positional construction with a fresh child list; subclasses retain keyword construction and their own name-cleaning behavior. The change is confined to parse_blob and works with both UnityPyBoost and the attrs fallback.

Measurements

Measurement Before After
Warmed test_save, median of five 30-call repeats 91.7 ms/call 35.0 ms/call
Fresh-process test_save, median of three pytest call durations 219 ms 168 ms
Frozen workload, sample medians across three rescores, two repetitions per sample 183.0-183.7 ms 72.7-78.1 ms
Frozen workload, median paired candidate/baseline ratios 1.0 0.414 / 0.397 / 0.425

Measurements used Python 3.12.13 and isolated benchmark cores. The warmed repository benchmark used:

python -m timeit -n 30 -r 5 -v \
  -s 'from tests.test_main import test_save; test_save()' 'test_save()'

The reported result is the median of the five per-repeat per-call times, rather than timeit's default best-repeat summary. Fresh-process timings came from pytest's JUnit call-only durations, alternating baseline/candidate order, and include first-call initialization; they are reported separately from the warmed speedup.

The frozen evaluator mirrors the sample load/save/reload cycle and also parses recorded blobs with the attrs fallback. Each score uses nine interleaved baseline/candidate pairs with shuffled sample order and two workload repetitions. It checks saved-byte hashes and every node field against an independent baseline oracle. The observed relative IQRs were 6.3%, 9.7%, and 6.1%, below its 20% limit; all three time ratios are below the required 0.80 threshold.

Validation

  • python -m pytest -q -p no:cacheprovider tests/test_typetree.py tests/test_main.py tests/test_UnityVersion.py tests/test_astc.py: 44 passed in 3.26s.
  • Full repository ruff check and ruff format --check: passed, 92 files already formatted (Ruff 0.17.3).
  • Supplemental baseline/candidate comparison: 400 matching cases, covering both node implementations, both byte orders, versions 10/18/19/23, repeated/reserved/Unicode/punctuation names, tree levels, empty blobs, reader positions, and exceptions.

AI assistance and provenance

Codex (gpt-6.1-sol) generated candidates tracked with Weco Observe. The selected source is the exact recorded step 5 of run 7e8dfc76-13b2-4288-817a-fb9410034f98, which contains 14 distinct valid non-baseline candidates. Candidate counts are from this run alone. Step 5 was chosen for its smaller change; the faster recorded step 10 also changes string decoding and tree assembly, for a measured gain within the harness noise limit relative to step 5.

Validation and source review were automated. Three preliminary rescore attempts were rejected because lint had left a .ruff_cache directory in the test tree; their results were retained. Moving that generated cache outside the tree left source bytes unchanged, and the three clean-tree scores above then passed.

Weco share link: https://dashboard.weco.ai/share/WjmBTYxucv7EwrkZmcRRZCwXLqXRvmcv

Cache clean names within each blob and recognize unchanged ASCII
identifiers before constructing nodes. Reuse the empty-name constructor
path for cached names and pass ordinary node fields positionally, while
preserving keyword construction and name cleaning for subclasses.

Warmed tests/test_main.py::test_save: 91.7 ms -> 35.0 ms per call
(median of five standard-library timeit repeats of 30 calls).
Three frozen-harness ratios: 0.414, 0.397, 0.425.
Native tests: 44 passed; full Ruff lint and formatting passed.

Co-Authored-By: Aiden <aiden@weco.ai>

This branch has not been deployed

No deployments
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