protocol: quote_big_ints is unguarded and costs ~40% of the apply frame #69
Labels
No labels
area:defold
area:docs
area:infra
area:protocol
area:testing
kind:bug
kind:chore
phase:breadth
phase:core
phase:hardening
phase:slice
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
yuris/spacetimedb-defold#69
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while re-measuring #30 for #66. This is the cheapest real optimisation the
measurement turned up, and it needs no protocol change, no API change, and no
time-slicing.
The finding
protocol.quote_big_ints(stdb/protocol.lua:323) has no guard. Its siblingquote_wide_ints(:807-810) has a necessary-condition guard and returns theinput unchanged after three C-level finds. So on any table without a 128-bit
column, the hand-written schema path pays a full
gsubscan over the whole frametext and another over every row, to rewrite nothing at all.
Measured on a 7-column bulk table with no wide-int column, native, medians over
44 samples at 10k rows:
At 50k: decode 76.6ms, quoting 77.5ms — quoting costs more than the JSON
parser. At 100k: 183.6 vs 177.7ms. In wasm at 10k: decode 23ms, quoting 20ms. It
is consistently 40–44% of the native frame.
Independently reproduced on the #66 branch:
dec=39% quo=42% bld=15%.Why the asymmetry is the whole story
The typed path is 13–20% slower overall, and the reason is visible once the
buckets are split: going typed at 10k rows makes the quote bucket fall from
12.2ms to 6.4ms — because per-row
quote_big_intsis replaced by the guardedquote_wide_ints, which short-circuits — whilebuildrises from 3.2 to12.5ms for
decode_valueper column. The guarded function is alreadydemonstrating the win.
What to do
Give
quote_big_intsthe same shape of necessary-condition guard: a cheap plainfindfor the__marker before committing to the patterngsub. If the framecarries no wrapped integer, return it untouched.
Expected saving on a table with no 128-bit column: most of 40% of the apply frame.
On a table that does have one, the guard costs one plain find.
Why this matters for #30 and #9
landing first, since it changes the thresholds time-slicing would be tuned
against — the 60fps cliff currently sits at ~6,100 rows.
with.
quote_big_intsexists only because JSON cannot carry a u128 exactly(
stdb/protocol.lua:299-320,:711-714), so BSATN deletes it — but so does aguard, for a fraction of the effort and with no protocol surface re-opened.
Acceptance
must keep passing.
change. u128 values must still survive as text.
Note on why this was invisible until now
The measurement apparatus needed to see it at all. #66 measures five buckets that
sum to the frame, and an earlier version anchored the apply on the first
json.decode— which structurally excluded the frame-level quoting scan,because
decode_serverquotes the whole frame text before it decodes anything.Anchoring on whichever instrumented call fires first is what made this cost
visible.
Closed by #71. Post-guard thresholds on the new
main(a6c2c66), for #67 to tune against.Native, pooled medians across the two engine processes each invocation runs,
STDB_BULK_RUNS=5:Crossings: 16.7ms at ~8500 rows, 100ms at ~42000. Previously 6100 and 31000 — the guard moved both out by ~40%. Wasm crosses 100ms at ~10700 rows (was 9800).
Bucket split at 10000 rows, paired against
mainin one session:Two cautions for #67, since both affect where a frame budget should sit:
buildis a residual, not a measurement —frame - pre - post - decode - quote. Post-guard it absorbs the guard's ownfind(2.2ms at 10k) plus the harness's wrapper around it (another 1.0ms). Real construction is still ~3.9ms. So the decode/store split that #67 slices along is roughly 12.4ms decode against 3.9ms store at 10k rows, i.e. decode is ~75% of the controllable work — not the ~85% estimated pre-#69, but still the dominant term and still the right place to yield.README's performance section was updated in the same PR to these numbers, including the two claims that went stale unfavourably: the codec-swap bound drops from a quoted 6–8x to ~2.4x native (the guard already collected the deletable part), and the generated-schema penalty rises from "about a tenth" to ~50%, since
decode_typednever paid the per-row big-int scan and so gained nothing here.