Skip to content

Stop re-pickling arguments that can never be pickled - #26

Merged
LuShadowX merged 4 commits into
mainfrom
perf-overhead
Aug 8, 2026
Merged

Stop re-pickling arguments that can never be pickled#26
LuShadowX merged 4 commits into
mainfrom
perf-overhead

Conversation

@LuShadowX

Copy link
Copy Markdown
Owner

Recording sqlparse took 16.0s against a 1.1s test suite. Profiling by stubbing out each layer showed the wrapper dispatch costs essentially nothing across 1.42M intercepted calls — the time is pickling, and more than half of it was pickling that failed.

38,627 pickle.dumps calls raised PicklingError and were thrown away. Two functions caused nearly all of them, both taking a lambda on every call: TokenList._token_matching and engine.grouping:_group. The pickler walks the entire TokenList before reaching the lambda, so it pays almost the full serialisation cost and then raises.

Duplicates back off and oversized inputs get abandoned, but a failure did neither — it counted the failure and returned, remembering nothing, so every later call retried for the whole run.

Targets whose arguments repeatedly fail to pickle are now abandoned, mirroring the existing oversized logic but with a much more generous threshold (16 against 3): a failure is usually evidence about the signature rather than the input, but not always, and a signature that genuinely cannot be pickled trips 16 immediately anyway.

Measured on sqlparse, interleaved A/B, three pairs

before after
record wall time 16.07 / 15.80 / 16.02s 6.36 / 6.32 / 6.35s
overhead vs bare suite 8.2x 3.7x

Coverage is unchanged, not approximately unchanged: 16,321 distinct inputs across 141 targets before and after, with per-target counts identical and no target losing a single input.

abandoned grows 38 → 42. All four additions had zero recorded inputs before, so they were silent gaps that check implicitly papered over; they are now declared. That is a net honesty gain, and it is why a threshold of 3 was rejected — it cost 66 real inputs from functions that take a callback on only some paths.

nodrift check HEAD on unchanged sqlparse still reports no behaviour change across all 16,321 inputs, so the speedup buys no false positives.

sys.monitoring was deliberately not used. It would replace the interception mechanism, which the profile shows costs about 1% of the overhead — the wrong lever, and 3.12+ only.

Also included: the dedup table is keyed on the pickled bytes rather than a digest of a digest, and calls skipped because their target was abandoned are counted so the run summary still adds up.

Closes #5

Recording sqlparse spent 5.2s of a 10s run on pickle attempts that
raised PicklingError, 38,627 of them, for two functions that take a
lambda on every call: TokenList._token_matching and grouping._group.
The pickler walks the whole argument graph before reaching the lambda,
so the full serialisation cost is paid and then discarded.

Duplicates back off and oversized inputs get abandoned, but a failure
did neither -- it incremented a counter and returned, so the next call
tried again, for the whole run.

Failures now abandon the target the way oversized inputs do, with a
more generous threshold (16 vs 3): a failure is usually evidence about
the signature rather than the input, but a function taking a callback
on only some paths deserves rope. Being generous is free, since a
signature that cannot be pickled fails thousands of times.

sqlparse: 8.66s -> 4.02s, 16,321 distinct inputs unchanged. The four
newly abandoned functions had zero recorded inputs before, so nothing
is lost -- they were silent gaps and are now declared ones.
The key was digest(["blob", len(blob), _cheap_hash(blob)]): a blake2b
pass over every blob, hex-encoded, JSON-serialised, then hashed again.
The bytes are already the identity of the input and are already in
hand, so they are the key. Exact rather than a digest of a digest, and
one fewer stored string per record.
Abandonment used to be rare enough that returning without touching a
counter did not show up. It is now the common reason a call is never
looked at, and an uncounted early return makes the run summary stop
adding up.
_cheap_hash and the digest import became dead when the pickled bytes
became the key. (fingerprint was already imported unused.)
@LuShadowX
LuShadowX merged commit 501e769 into main Aug 8, 2026
10 checks passed
@LuShadowX
LuShadowX deleted the perf-overhead branch August 8, 2026 05:51
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.

Reduce recording overhead further (now ~8x, was 24x)

1 participant