perf(encode): make the encoding bench reflect a real Node.js HTTP request#8668
Conversation
…quest The previous fixture was 30 copies of one span with three meta entries and three metrics, so span 1 warmed the string cache and every later iteration replayed identical state -- anything that moved per-span work beyond cache filling read as noise. The new fixture is a 30-span Express-request shape (server root + middleware fan + Postgres / Redis / HTTP-client / DNS spans) with one error span carrying a 1.5 KB `error.stack`. Hot keys (`span.kind`, `component`, `runtime-id`, env, version) reuse the same strings across spans so the cache still warms after one iteration but the meta-map hot loop and the wire output run on production-shaped data.
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: a5cd63b | Docs | Datadog PR Page | Give us feedback! |
Overall package sizeSelf size: 5.96 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.0.1 | 82.56 kB | 817.39 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #8668 +/- ##
==========================================
- Coverage 40.95% 39.78% -1.17%
==========================================
Files 286 495 +209
Lines 15987 30639 +14652
Branches 3532 3532
==========================================
+ Hits 6548 12191 +5643
- Misses 9439 18448 +9009 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
BenchmarksBenchmark execution time: 2026-05-27 21:13:28 Comparing candidate commit a5cd63b in PR branch Found 0 performance improvements and 58 performance regressions! Performance is the same for 1434 metrics, 101 unstable metrics. scenario:encoders-0.4-20
scenario:encoders-0.4-22
scenario:encoders-0.4-24
scenario:encoders-0.4-events-legacy-20
scenario:encoders-0.4-events-legacy-22
scenario:encoders-0.4-events-legacy-24
scenario:encoders-0.4-events-native-20
scenario:encoders-0.4-events-native-22
scenario:encoders-0.4-events-native-24
scenario:encoders-0.5-20
scenario:encoders-0.5-22
scenario:encoders-0.5-24
scenario:encoders-0.5-events-legacy-20
scenario:encoders-0.5-events-legacy-22
scenario:encoders-0.5-events-legacy-24
|
The previous fixture's trace was built once and reused for 5000 encodes unchanged: the encoder's string cache, magnitude-branch predictor, and per-span ID reads all settled to a single state and held it, so any optimisation that depended on hot-vs-cold variance read as noise. `tickTrace(trace, iteration)` now runs before each encode and rewrites every per-request dynamic field in place (timestamps, durations, ID buffer low halves, `db.row_count`, root-span route / URL / resource / status / client IP, and error type / message / stack). Hot strings reused across spans stay constant so the cache still warms after one iteration. The `+ iteration * 4096` step on `span.start` is the smallest increment visible past the IEEE-754 double's ULP at the ~1.7e18 nano-timestamp magnitude (256 nanos). A real fix for that precision loss needs the span to carry `start` as `BigInt`, which is a separate change on the tracer side, not here.
…quest (#8668) * bench(encode): make the encoding bench reflect a real Node.js HTTP request The previous fixture was 30 copies of one span with three meta entries and three metrics, so span 1 warmed the string cache and every later iteration replayed identical state -- anything that moved per-span work beyond cache filling read as noise. The new fixture is a 30-span Express-request shape (server root + middleware fan + Postgres / Redis / HTTP-client / DNS spans) with one error span carrying a 1.5 KB `error.stack`. Hot keys (`span.kind`, `component`, `runtime-id`, env, version) reuse the same strings across spans so the cache still warms after one iteration but the meta-map hot loop and the wire output run on production-shaped data. * bench(encode): vary every per-request field across iterations The previous fixture's trace was built once and reused for 5000 encodes unchanged: the encoder's string cache, magnitude-branch predictor, and per-span ID reads all settled to a single state and held it, so any optimisation that depended on hot-vs-cold variance read as noise. `tickTrace(trace, iteration)` now runs before each encode and rewrites every per-request dynamic field in place (timestamps, durations, ID buffer low halves, `db.row_count`, root-span route / URL / resource / status / client IP, and error type / message / stack). Hot strings reused across spans stay constant so the cache still warms after one iteration. The `+ iteration * 4096` step on `span.start` is the smallest increment visible past the IEEE-754 double's ULP at the ~1.7e18 nano-timestamp magnitude (256 nanos). A real fix for that precision loss needs the span to carry `start` as `BigInt`, which is a separate change on the tracer side, not here.
Summary
The old encoding bench was 30 clones of one span with three meta keys
(
a/hello/and) and three metrics. Span 1 filled the string cache;every iteration after that replayed identical state, so anything that
moved per-span work beyond cache filling read as noise.
The new fixture (
trace-fixture.js) is a 30-span Express-request shape:server root, middleware fan, a handful of Postgres / Redis / HTTP-client
spans, two DNS lookups, and one error span carrying a 1.5 KB
error.stack. Hot keys (span.kind,component,runtime-id, env,version) reuse the same strings across spans, so the cache still warms
after one iteration but the meta-map hot loop and the wire output run
on production-shaped data.
Why
The sirun bench-comparison comment reads this fixture as a baseline
regression in any PR that introduces it -- the new fixture does roughly
5x the encode work per iteration. Landing it on its own absorbs the
step once, so encoder-side PRs that follow can be compared apples to
apples instead of new-fixture-vs-old-fixture.