Skip to content

Commit 0de9917

Browse files
committed
Add Live Debugger runtime benchmark coverage
The browser Live Debugger instrumentation needs repeatable runtime overhead checks before transform changes land. Add an opt-in Playwright benchmark that compares baseline, control, and instrumented workloads in the same browser session with dormant probe hooks installed. Report conservative per-call overhead bounds with confidence intervals, A/A diagnostics, block bootstrap checks, and PR comment output. Wire the benchmark into CI as a non-blocking job, share Playwright setup between jobs, upload raw samples as artifacts, and document how contributors should run and interpret the benchmark.
1 parent 0440507 commit 0de9917

22 files changed

Lines changed: 2865 additions & 43 deletions

File tree

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ module.exports = {
407407
'packages/plugins/**/scripts/**/*',
408408
'packages/tests/src/_jest/**/*',
409409
'packages/tests/src/_playwright/**/*',
410+
'packages/tests/src/bench/**/*',
410411
'packages/tests/src/e2e/**/*',
411412
],
412413
rules: {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Setup Playwright build
2+
description: Install dependencies, Playwright browsers, and prebuilt plugins for Playwright jobs.
3+
runs:
4+
using: composite
5+
steps:
6+
- name: Install Node
7+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
8+
with:
9+
node-version-file: "package.json"
10+
11+
- name: Cache build:all
12+
id: cache-build
13+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
14+
with:
15+
path: packages/published/**/dist
16+
key: node18-cache-build-${{ hashFiles('packages/core/**', 'packages/factory/**',
17+
'packages/plugins/**', 'packages/published/**',
18+
'packages/tools/src/**', 'yarn.lock') }}
19+
20+
- name: Cache playwright binaries
21+
id: cache-playwright-binaries
22+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
23+
with:
24+
path: |
25+
~/.cache/ms-playwright
26+
~/Library/Caches/ms-playwright
27+
%USERPROFILE%\AppData\Local\ms-playwright
28+
key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }}
29+
30+
- run: yarn install --immutable --immutable-cache
31+
shell: bash
32+
33+
- name: Install playwright
34+
run: yarn workspace @dd/tests playwright install --with-deps
35+
shell: bash
36+
37+
- name: Build all plugins
38+
if: steps.cache-build.outputs.cache-hit != 'true'
39+
run: yarn build:all-no-types
40+
shell: bash
41+
42+
- name: Save playwright cache
43+
if: always() && steps.cache-playwright-binaries.outputs.cache-hit != 'true'
44+
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
45+
with:
46+
path: |
47+
~/.cache/ms-playwright
48+
~/Library/Caches/ms-playwright
49+
%USERPROFILE%\AppData\Local\ms-playwright
50+
key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }}

.github/workflows/ci.yaml

Lines changed: 129 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -64,29 +64,7 @@ jobs:
6464
steps:
6565
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
6666

67-
- name: Install Node
68-
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
69-
with:
70-
node-version-file: "package.json"
71-
72-
- name: Cache build:all
73-
id: cache-build
74-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
75-
with:
76-
path: packages/published/**/dist
77-
key: node18-cache-build-${{ hashFiles('packages/core/**', 'packages/factory/**',
78-
'packages/plugins/**', 'packages/published/**',
79-
'packages/tools/src/**', 'yarn.lock') }}
80-
81-
- name: Cache playwright binaries
82-
id: cache-playwright-binaries
83-
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
84-
with:
85-
path: |
86-
~/.cache/ms-playwright
87-
~/Library/Caches/ms-playwright
88-
%USERPROFILE%\AppData\Local\ms-playwright
89-
key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }}
67+
- uses: ./.github/actions/setup-playwright-build
9068

9169
- name: Configure Datadog Test Optimization
9270
uses: datadog/test-visibility-github-action@f76512a963e7375dab9ad7f1abc0cacd41806c5c # v2.6.0
@@ -96,37 +74,146 @@ jobs:
9674
api_key: ${{secrets.DATADOG_API_KEY}}
9775
site: datadoghq.com
9876

99-
- run: yarn install --immutable --immutable-cache
100-
101-
- name: Install playwright
102-
run: yarn workspace @dd/tests playwright install --with-deps
103-
104-
- name: Build all plugins
105-
if: steps.cache-build.outputs.cache-hit != 'true'
106-
run: yarn build:all-no-types
107-
10877
- run: yarn test:e2e
10978
env:
11079
NODE_OPTIONS: -r ${{env.DD_TRACE_PACKAGE}}
11180
DD_TAGS: type:e2e
11281
DD_ENV: ci
11382

114-
- name: Save playwright cache
115-
if: always() && steps.cache-playwright-binaries.outputs.cache-hit != 'true'
116-
id: save-playwright-cache
117-
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
83+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
84+
if: ${{ failure() }}
11885
with:
86+
name: playwright
11987
path: |
120-
~/.cache/ms-playwright
121-
~/Library/Caches/ms-playwright
122-
%USERPROFILE%\AppData\Local\ms-playwright
123-
key: cache-playwright-binaries-${{ hashFiles('yarn.lock') }}
88+
packages/tests/playwright-report
89+
packages/tests/test-results
90+
retention-days: 3
91+
92+
runtime-bench-preflight:
93+
timeout-minutes: 20
94+
95+
name: Live Debugger runtime benchmark preflight
96+
runs-on: ubuntu-latest
97+
if: ${{ github.event_name == 'pull_request' && !startsWith(github.ref_name, 'mq-working-branch-') }}
98+
outputs:
99+
should-run: ${{ steps.output-check.outputs.should-run || steps.changed-files.outputs.should-run }}
100+
reason: ${{ steps.output-check.outputs.reason || steps.changed-files.outputs.reason }}
101+
env:
102+
FORCE_COLOR: true
103+
104+
steps:
105+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
106+
with:
107+
fetch-depth: 0
108+
109+
- name: Install Node
110+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
111+
with:
112+
node-version-file: "package.json"
113+
114+
- run: yarn install --immutable --immutable-cache
115+
116+
- name: Check benchmark and CI changes
117+
id: changed-files
118+
run: node packages/tests/src/bench/liveDebuggerRuntime/preflight.js --changed-files --base-ref=origin/${{ github.base_ref }}
119+
120+
- name: Check benchmark output changes
121+
if: steps.changed-files.outputs.should-run != 'true'
122+
id: output-check
123+
run: node packages/tests/src/bench/liveDebuggerRuntime/preflight.js --compare-output --base-ref=origin/${{ github.base_ref }}
124+
125+
runtime-bench:
126+
timeout-minutes: 30
127+
128+
name: Live Debugger runtime benchmark
129+
runs-on: ubuntu-latest
130+
needs: runtime-bench-preflight
131+
if: ${{ needs.runtime-bench-preflight.outputs.should-run == 'true' }}
132+
continue-on-error: true
133+
permissions:
134+
contents: read
135+
pull-requests: write
136+
env:
137+
FORCE_COLOR: true
138+
139+
steps:
140+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
141+
142+
- uses: ./.github/actions/setup-playwright-build
143+
144+
- name: Run runtime benchmark
145+
run: yarn workspace @dd/tests bench:live-debugger:runtime
146+
147+
- name: Render benchmark comment
148+
if: always()
149+
run: |
150+
BENCH_TMP_DIR="$(node -e "process.stdout.write(require('os').tmpdir())")"
151+
COMMENT_PATH="$BENCH_TMP_DIR/live-debugger-runtime-bench-comment.md"
152+
RESULTS_PATH="$(ls -t "$BENCH_TMP_DIR"/live-debugger-runtime-bench-results-*.json 2>/dev/null | head -n 1)"
153+
if [ -f "$COMMENT_PATH" ]; then
154+
cp "$COMMENT_PATH" runtime-bench-comment.md
155+
else
156+
{
157+
echo '<!-- ld-runtime-bench -->'
158+
echo '## Live Debugger Runtime Benchmark'
159+
echo
160+
echo 'Benchmark results were not produced. Check the workflow logs for details.'
161+
} > runtime-bench-comment.md
162+
fi
163+
if [ -n "$RESULTS_PATH" ] && [ -f "$RESULTS_PATH" ]; then
164+
cp "$RESULTS_PATH" runtime-bench-results.json
165+
fi
166+
167+
- name: Find benchmark comment
168+
if: always()
169+
id: benchmark-comment
170+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
171+
with:
172+
script: |
173+
const marker = '<!-- ld-runtime-bench -->';
174+
const comments = await github.paginate(github.rest.issues.listComments, {
175+
owner: context.repo.owner,
176+
repo: context.repo.repo,
177+
issue_number: context.issue.number,
178+
});
179+
const comment = comments.find((candidate) => candidate.body?.includes(marker));
180+
core.setOutput('comment-id', comment?.id || '');
181+
182+
- name: Create benchmark comment
183+
if: always() && steps.benchmark-comment.outputs.comment-id == ''
184+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
185+
with:
186+
script: |
187+
const fs = require('fs');
188+
const body = fs.readFileSync('runtime-bench-comment.md', 'utf8');
189+
await github.rest.issues.createComment({
190+
owner: context.repo.owner,
191+
repo: context.repo.repo,
192+
issue_number: context.issue.number,
193+
body,
194+
});
195+
196+
- name: Update benchmark comment
197+
if: always() && steps.benchmark-comment.outputs.comment-id != ''
198+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
199+
with:
200+
script: |
201+
const fs = require('fs');
202+
const body = fs.readFileSync('runtime-bench-comment.md', 'utf8');
203+
await github.rest.issues.updateComment({
204+
owner: context.repo.owner,
205+
repo: context.repo.repo,
206+
comment_id: Number('${{ steps.benchmark-comment.outputs.comment-id }}'),
207+
body,
208+
});
124209
125210
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
126-
if: ${{ failure() }}
211+
if: always()
127212
with:
128-
name: playwright
213+
name: live-debugger-runtime-bench-results
129214
path: |
215+
runtime-bench-comment.md
216+
runtime-bench-results.json
130217
packages/tests/playwright-report
131218
packages/tests/test-results
132219
retention-days: 3
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Contributing to Live Debugger <!-- #omit in toc -->
2+
3+
Developer notes for the Live Debugger plugin.
4+
5+
## Table of content <!-- #omit in toc -->
6+
7+
<!-- #toc -->
8+
- [Development workflow](#development-workflow)
9+
- [Runtime benchmark](#runtime-benchmark)
10+
- [Running it](#running-it)
11+
- [What it measures](#what-it-measures)
12+
- [How to interpret the results](#how-to-interpret-the-results)
13+
- [Methodology](#methodology)
14+
- [Caveats](#caveats)
15+
<!-- #toc -->
16+
17+
## Development workflow
18+
19+
Use the [root contributor guide](../../../CONTRIBUTING.md) for repository setup, formatting, and release process. This page covers the Live Debugger-specific checks that are easy to miss when changing the plugin.
20+
21+
Run the focused unit suite while iterating:
22+
23+
```bash
24+
yarn test:unit packages/plugins/live-debugger
25+
```
26+
27+
Run the package typecheck when changing exported types, option handling, or transform internals:
28+
29+
```bash
30+
yarn workspace @dd/live-debugger-plugin typecheck
31+
```
32+
33+
When changing instrumentation output, add or update cases in [`src/transform/index.test.ts`](./src/transform/index.test.ts). If the generated before/after shape changes in a way users or reviewers should understand, update [`EXAMPLES.md`](./EXAMPLES.md) alongside the tests.
34+
35+
Changes that affect source positions, injected wrappers, return rewriting, or error handling should preserve source maps. Cover those cases in [`src/sourcemap.integration.test.ts`](./src/sourcemap.integration.test.ts).
36+
37+
The Babel packages and `magic-string` are optional peer dependencies for consumers. Keep the transform dependencies lazy-loaded and preserve the user-facing missing-dependency error path. Update [`src/transform/lazy-deps.test.ts`](./src/transform/lazy-deps.test.ts) when touching dependency loading.
38+
39+
Generated code should keep the dormant runtime path small: call `$dd_probes(functionId)` first, and only call `$dd_entry`, `$dd_return`, or `$dd_throw` when a probe is active. Preserve the no-SDK fallback injected from [`src/runtime-bootstrap.ts`](./src/runtime-bootstrap.ts).
40+
41+
When adding or changing `liveDebugger` configuration, update [`src/types.ts`](./src/types.ts), [`src/validate.ts`](./src/validate.ts), [`src/validate.test.ts`](./src/validate.test.ts), and the consumer-facing [`README.md`](./README.md).
42+
43+
## Runtime benchmark
44+
45+
The opt-in browser benchmark measures the dormant runtime overhead added by Live Debugger instrumentation. It compares instrumented code against equivalent uninstrumented code, back-to-back in the same browser session, while SDK-like dormant probe hooks are installed.
46+
47+
### Running it
48+
49+
Run it locally with:
50+
51+
```bash
52+
yarn workspace @dd/tests bench:live-debugger:runtime
53+
```
54+
55+
For a faster loop, pass a browser project:
56+
57+
```bash
58+
yarn workspace @dd/tests bench:live-debugger:runtime --project chrome
59+
```
60+
61+
The terminal output prints one row per browser and workload (`Tiny`, `Hot`). Browser projects run serially to reduce CPU contention. The benchmark uses one fixed bundler so the report focuses on runtime overhead, not on bundler-to-bundler differences.
62+
63+
### What it measures
64+
65+
Each sample measures three variants:
66+
67+
- **baseline**: the uninstrumented workload.
68+
- **control**: the same baseline function measured a second time. This is an A/A diagnostic for timing noise in the benchmark apparatus.
69+
- **instrumented**: the same workload after Live Debugger instrumentation, with dormant SDK hooks installed.
70+
71+
The reporter estimates overhead from `instrumented - control`. That direct paired difference avoids the old correlated-interval comparison against the shared baseline sample. The `control - baseline` result is still shown as the A/A diagnostic; it should be centered around zero if the browser session is quiet enough to trust.
72+
73+
There are two workloads because one number cannot describe every runtime shape:
74+
75+
- **Tiny** calls one very small instrumented function. It is the best row for answering: "what is the smallest cost we can measure for one dormant instrumented call?" Since the function does almost no work, its baseline time is tiny too. That means a small nanosecond cost can look like a large percentage.
76+
- **Hot** runs an uninstrumented loop that calls a small instrumented kernel many times. It is the best row for answering: "what happens when an instrumented function sits on a hot path?" This row includes the cost of the dormant hooks and any optimizer disruption from the instrumented function shape, such as losing an inlining opportunity.
77+
78+
Read them together. `Tiny` shows the minimum cost and the measurement floor. `Hot` shows the repeated-call hot-path cost. If `Hot` is higher than `Tiny` in nanoseconds per call, the gap is the extra cost from the hot-path shape in this benchmark. If `Tiny` is higher in percentage, that usually means the denominator is much smaller, not that `Tiny` has a larger absolute cost.
79+
80+
### How to interpret the results
81+
82+
Start with three columns:
83+
84+
- **per-call overhead upper**: the headline number. It is the conservative upper bound for dormant overhead per instrumented function call, reported in nanoseconds.
85+
- **quality**: whether the row is safe to read. `clean` means use the row, `caution` means it is usable but worth rerunning if the number matters, and `unreliable` means rerun before drawing conclusions.
86+
- **overhead upper**: the same result as a workload-level percentage. Use this as context, not as the main comparison between `Tiny` and `Hot`.
87+
88+
Prefer the nanosecond number when comparing workloads. It puts `Tiny` and `Hot` on the same per-call scale. The percentage can look inverted because it divides by the workload's baseline time. `Tiny` does almost no work, so a small absolute cost can become a large percentage. `Hot` does more baseline work, so a larger absolute cost can still be a smaller percentage.
89+
90+
Example: if `Tiny` reports `<= 1.5 ns` and `Hot` reports `<= 5.0 ns`, the benchmark is saying the hot-path shape costs more per instrumented call. If those same rows report `Tiny <= 40%` and `Hot <= 5%`, that does not contradict the nanosecond result. It only means `Tiny` started from a much smaller baseline.
91+
92+
The other diagnostic columns explain why a row got its `quality` verdict:
93+
94+
- **95% CI**: the signed estimate range for `instrumented - control`. If it sits near zero, the overhead was too small to resolve clearly.
95+
- **A/A diag**: `control - baseline`. This is the benchmark checking itself by timing the baseline code twice. A small value is fine; a value as large as the measured effect means the browser session was noisy.
96+
- **Block CI** and **acf(1)**: checks for timing drift across samples. If the block interval tells the same story as the main interval, the row is usually fine even when `acf(1)` is non-zero.
97+
- **Samples**: how many samples were recorded, plus trimming and outlier diagnostics. A few outliers are expected in browser timing. The row only becomes suspect when one side has enough outliers to survive the 20% trim.
98+
99+
The benchmark treats tiny "speed-ups" as measurement noise. Instrumentation only adds work, but separate baseline and instrumented bundles can land in slightly different code layouts. Below about `0.5 ns/call`, that layout noise is roughly the same size as the effect being measured, so the row is reported as clean but unresolved rather than as a real speed-up.
100+
101+
### Methodology
102+
103+
The benchmark tries to make each browser comparison fair and repeatable:
104+
105+
- It serves the page with cross-origin isolation headers so `performance.now()` has better precision.
106+
- It warms up each workload before measuring, then calibrates the batch size, then warms up again with the calibrated size.
107+
- It calibrates against the slowest variant. This keeps the slow instrumented batches from becoming much longer than the baseline batches, which would make the run more vulnerable to JIT warm-up or thermal drift.
108+
- It records `baseline`, `control`, and `instrumented` back-to-back in rotating forward/reverse order. That keeps each variant from always running first, middle, or last.
109+
- It rounds the sample count to a full counterbalancing period, `2 * variantCount`, so every timing position is represented evenly.
110+
111+
The reported point estimate is a trimmed mean of `instrumented - control`: the benchmark drops the noisiest 20% on each side and averages the middle. Confidence intervals are bootstrapped from the same paired samples. The percentage column uses the same paired data, but divides by the baseline workload time.
112+
113+
The code uses more specific statistical machinery than this section describes, but the practical rule is simple: trust `clean` rows, rerun `unreliable` rows, and compare `Tiny` and `Hot` primarily on `per-call overhead upper`.
114+
115+
### Caveats
116+
117+
Do not compare absolute timings across unrelated machines. Treat the report as a back-to-back comparison from one browser session.
118+
119+
The benchmark builds separate baseline and instrumented bundles. That is necessary for the comparison, but it also means the browser may lay out or optimize the two bundles slightly differently. The A/A diagnostic can catch noise in the baseline path, but it cannot see every instrumented-bundle-specific effect. This is why `Tiny` and `Hot` should be read as a bracket rather than as one universal overhead number.
120+
121+
`Tiny` is close to the measurement floor on fast engines. A tiny negative result, especially below about `0.5 ns/call`, should be read as "too small to resolve", not as instrumentation making code faster.
122+
123+
Browser timings can be spiky or coarsely quantized. The trimmed mean handles ordinary spikes, and the `outliers` reason appears only when the spike pattern is large enough to threaten the estimate. If a row says `unreliable (outliers)`, rerun before trusting it.

packages/tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"./_jest/helpers/*": "./src/_jest/helpers/*.ts"
1818
},
1919
"scripts": {
20+
"bench:live-debugger:runtime": "BUILD_PLUGINS_ENV=test FORCE_COLOR=true PLAYWRIGHT_REQUESTED_BUNDLERS=rspack playwright test --config=playwright.live-debugger-runtime.config.ts",
2021
"build": "yarn clean && tsc",
2122
"clean": "rm -rf dist",
2223
"test:e2e": "BUILD_PLUGINS_ENV=test FORCE_COLOR=true playwright test",

0 commit comments

Comments
 (0)