Skip to content

Annotation Form Performance Baseline

This benchmark records the v1 annotation-form baseline before AF2 and its Angular 22 comparison point after Track 0 and Phase 1. Phase 3 uses the Angular 22 measurement as its before/after reference.

Results

Date Commit Machine Initial render Study navigation Keystroke-to-paint p95 Tab switch
2026-08-04 72f38768c local dev 4461 ms 93 ms 388 ms
2026-08-06 4c1af0a20 GitHub Actions ubuntu-latest / Angular 22 8210 ms 2665 ms 111.3 ms 3679 ms

Seeded shape

The Playwright scenario creates one study and 100 textbox annotation questions: 60 Study questions and 40 Cohort questions. The cohort set has 20 root questions and 20 child questions whose Target.ParentId values each point to a root. It then creates 15 cohort units through the annotation UI before sampling keystroke-to-paint and tab-switch latency.

Invocation

From e2e/, with the local E2E infrastructure and .NET services running:

pnpm test:perf

The test attaches the four metrics as perf JSON to the Playwright result and also prints them with console.table.

Angular 22 re-measure (2026-08-06)

The repository's isolated full-E2E workflow measured the Angular 22 platform on PR #2702, run 31059742925. The scenario passed on its second retry after two study-navigation timeouts; the successful attempt emitted the values above. Runner and retry variability mean these are a comparison baseline rather than a product service-level objective. Phase 3 must run the same isolated scenario and report its delta against this row.

AF2 Phase 3.5 performance gate

The AF2 benchmark is a separate Playwright scenario so the v1 history above remains unchanged. It overrides only the browser response for appConfig.env.json, setting annotationFormV2 to "true" for that test; no committed runtime configuration or default feature flag is changed.

The scenario creates one ordinary, non-extraction review stage with 1,000 supported textbox questions split evenly between Study and Cohort. AF2 has one synthetic Study unit in this shape; Cohort deliberately has no extraction units, which makes switching to its empty unit workspace a visible transition without broadening the Phase 3 non-experiment seed model.

Date Commit Machine AF2 host-to-first interactive Route-to-first interactive (diagnostic) Edit-to-microtask-settle p95 Edit-to-paint p95 (diagnostic) Category switch Mounted controls / units Forced-GC heap plateau spread Route-to-first-interactive delta vs Angular 22 v1
2026-08-10 17cc57e28 GitHub Actions ubuntu-24.04 / Angular 22 662.7 ms 8135 ms 14 ms 16.3 ms 66.5 ms 6 / 1 229,016 B −75 ms (0.9% faster)

A second ordinary, non-extraction scenario persists 200 Cohort label annotations through the authenticated review-session API. The normal AF2 load path then projects those labels into units and reconciles four supported child questions per unit, giving a representative repeated question graph while exercising the twenty-page bounded workspace without timing setup dialogs.

Date Commit Machine Session seed (diagnostic) Page-turn p95 Mounted controls / units / cards Forced-GC heap plateau spread
2026-08-10 17cc57e28 GitHub Actions ubuntu-24.04 / Angular 22 62.2 ms 173.4 ms 40 / 10 / 10 1,626,948 B (1.55 MiB)

The first-interactive budget combines host mount to the first painted, enabled Open all action with that action's click to the first painted, usable control. Both segments are recorded inside the browser so Playwright protocol and actionability waits between them are excluded, while blocked main-thread and rendering work remains counted. The separate route diagnostics include API latency and application startup outside the form so the cold-navigation result remains comparable with the v1 baseline.

The hard budgets are: AF2 host-to-first interactive under 1,500 ms, application edit-to-microtask-settle p95 under 16 ms, visible category switching under 250 ms, no more than 20 mounted controls or 10 mounted units, and no more than 8 MiB spread across four forced-GC samples after warm-up. The one-frame paint sample is diagnostic because frame scheduling is not the application-processing time. The 200-unit workload additionally requires page-turn p95 under 250 ms, at most 40 mounted controls and 10 mounted units/cards, and the same 8 MiB heap plateau.

These measurements were attached by the normal run:e2e-full execution on PR #2710, run 31387747461. All 18 Playwright scenarios passed (one unrelated bulk-update scenario passed on its normal retry). The run was marked unsuccessful only after the tests, when the workflow invoked teardown a second time while the Playwright web server still occupied its configured port. That shared teardown defect was fixed and merged in PR #2722; the next exact-head run supplies the final clean workflow gate. It does not change these browser measurements from commit 17cc57e28.

AF2 Phase 4 PR 4 re-run (2026-08-11)

The Phase-4 plan re-runs this gate on any PR that changes mounted-row rendering. PR 4 adds the dormant outcome UI and mounts the outcome summary table inside an Experiment unit's form, so the whole benchmark was re-run at that branch head. All three scenarios passed both Playwright projects (9/9 tests) with --workers=1.

Date Commit Machine AF2 host-to-first interactive Route-to-first interactive (diagnostic) Edit-to-microtask-settle p95 Edit-to-paint p95 (diagnostic) Category switch Mounted controls / units Forced-GC heap plateau spread
2026-08-11 a13310901 local dev 397.9 ms 4776 ms 8.3 ms 16.1 ms 70.3 ms 6 / 1 259,816 B
Date Commit Machine Session seed (diagnostic) Page-turn p95 Mounted controls / units / cards Forced-GC heap plateau spread
2026-08-11 a13310901 local dev 126.0 ms 131.5 ms 40 / 10 / 10 1,532,624 B (1.46 MiB)

Every hard budget holds with margin: host-to-first-interactive 397.9 ms against 1,500 ms, edit-to-microtask-settle p95 8.3 ms against 16 ms, category switch 70.3 ms against 250 ms, page-turn p95 131.5 ms against 250 ms, and both heap plateaus far inside 8 MiB. Mounted controls and units are unchanged from the Phase-3.5 rows (6 / 1 and 40 / 10 / 10), which is the expected result: the outcome UI is dormant and off-route, so no scenario in this benchmark reaches an Experiment unit and none of its controls can enter the mounted budget.

These rows are local dev, not GitHub Actions, so they are not directly comparable with the ubuntu-24.04 rows above — they establish that the branch holds every budget, and the run:e2e-full label supplies the CI-measured comparison on the same runner class as the Phase-3.5 row.

AF2 Phase 4 PR 5 re-run (2026-08-11)

PR 5 is the activation PR: eligibility now admits extraction stages and the Experiment category on stage-review, so the Experiment tab and the outcome summary table it mounts are renderable for the first time. That changes what the form can mount, so the Phase-3 gate was re-run at this branch head. All three scenarios passed both Playwright projects (6/6 tests, --workers=1).

Date Commit Machine AF2 host-to-first interactive Route-to-first interactive (diagnostic) Edit-to-microtask-settle p95 Edit-to-paint p95 (diagnostic) Category switch Mounted controls / units Forced-GC heap plateau spread
2026-08-11 d056e8678 local dev 715.9 ms 5015 ms 6.4 ms 16.3 ms 38.7 ms 6 / 1 222,368 B
Date Commit Machine Session seed (diagnostic) Page-turn p95 Mounted controls / units / cards Forced-GC heap plateau spread
2026-08-11 d056e8678 local dev 117.6 ms 127.4 ms 40 / 10 / 10 193,940 B (0.18 MiB)

Every hard budget holds with margin: host-to-first-interactive 715.9 ms against 1,500 ms, edit-to-microtask-settle p95 6.4 ms against 16 ms, category switch 38.7 ms against 250 ms, page-turn p95 127.4 ms against 250 ms, and both heap plateaus far inside 8 MiB. Mounted controls and units are unchanged from the Phase-3.5 and PR-4 rows (6 / 1 and 40 / 10 / 10).

That last number is the one this PR had to prove. The Experiment category is now a real, selectable tab on every AF2 stage-review form, but the mounted-row budgets are per-category and per-page: an empty Experiment workspace mounts nothing, and the benchmark's two AF2 scenarios are non-extraction stages with no experiments, so the extra tab costs one more mat-tab-link and no mounted form rows. The outcome matrix itself only mounts inside an open Experiment unit's form, which is bounded by the same ten-unit page as every other category.

The v1 baseline scenario in the same run measured 4,816 ms initial render, 1,600 ms study navigation, 94.1 ms keystroke p95 and 1,739 ms tab switch on this machine — the local-dev comparison point for the AF2 route diagnostic above.

These rows are local dev, not GitHub Actions, so they are not directly comparable with the ubuntu-24.04 rows above — they establish that the branch holds every budget. Applying the run:e2e-full label to the PR supplies the CI-measured comparison on the same runner class as the Phase-3.5 row.

AF2 Phase 4 PR 5 fix round: the outcome matrix enters the gate (2026-08-11)

The PR-5 row above measures the two AF2 scenarios that existed at the time, and both are non-extraction stages with no experiments — so neither ever mounts an outcome matrix, and the activation claim did not cover the DOM PR 5 makes reachable. A third AF2 scenario now does.

AF2 extraction outcome-matrix performance gate. One extraction stage with 20 selected Study questions; its question set therefore also carries the whole system extraction set (Stage.AllStageAnnotationQuestions unions AnnotationQuestion.SystemQuestionIds, and selectAnnotationQuestionsForCurrentStage mirrors that union). A topology of 4 experiments x 6 cohorts x 8 outcomes is persisted through the authenticated session API, so the ordinary AF2 load path builds the matrices: 48 cells per experiment, 192 mounted at once with all four experiments open on one workspace page — the worst case the ten-unit page can produce for this shape.

Sampled: switching away from the Experiment workspace (all four matrices unmount), switching back to it (all 192 cells mount), opening a cell editor, the mounted matrix-cell and unit counts, and the forced-GC heap plateau across four category cycles.

Date Commit Machine Topology seed (diagnostic) Route-to-host (diagnostic) Category switch away Category switch to matrix Cell editor open Mounted matrix cells / units Forced-GC heap plateau spread
2026-08-11 b728e1521 local dev 48.9 ms 3888 ms 25.2 ms 119.3 ms 43.9 ms 192 / 4 156,612 B

Budgets hold, with margin. Category switching 119.3 ms against the 250 ms budget; the cell editor opens in 43.9 ms against the same 250 ms interaction budget; mounted units 4, inside the 10-unit page budget; heap plateau 156,612 B (0.15 MiB) against 8 MiB. The 192 mounted matrix cells are not covered by the 20-mounted-control budget and must not be: that budget counts mounted question controls (syrf-answer-row inputs), and a matrix cell is a summary button that opens its editor in a dialog, so the cells add DOM nodes but no form controls. It is recorded as its own number rather than folded into an existing one.

The other three scenarios were re-run unchanged at the same head, all passing (7/7 tests, --workers=1):

Date Commit Machine AF2 host-to-first interactive Route-to-first interactive (diagnostic) Edit-to-microtask-settle p95 Edit-to-paint p95 (diagnostic) Category switch Mounted controls / units Forced-GC heap plateau spread
2026-08-11 b728e1521 local dev 316.8 ms 4195 ms 5.3 ms 16.3 ms 37.8 ms 6 / 1 213,084 B
Date Commit Machine Session seed (diagnostic) Page-turn p95 Mounted controls / units / cards Forced-GC heap plateau spread
2026-08-11 b728e1521 local dev 117.7 ms 122.7 ms 40 / 10 / 10 1,535,288 B (1.46 MiB)

The v1 baseline scenario in the same run measured 4,334 ms initial render, 1,333 ms study navigation, 65.9 ms keystroke p95 and 1,509 ms tab switch.

These rows are local dev, not GitHub Actions, so they are not directly comparable with the ubuntu-24.04 rows above — they establish that the branch holds every budget on a matrix-bearing extraction stage. Applying the run:e2e-full label supplies the CI-measured comparison on the same runner class as the Phase-3.5 row.

Measurement notes for the matrix scenario

  • The category tab clicks and the Open all click are dispatched rather than driven through the mouse. A fully open Experiment workspace makes the page tall enough that the fixed project navigation overlaps the form's tab strip and Playwright refuses the real click; the v1 baseline scenario already dispatches for the same reason. The sample still measures the browser's own work.
  • The warm-up switch to the Experiment category is retried. The store re-selects a populated category on every hydrate (applySnapshot's populatedCategories fallback), and an extraction stage's Experiment units only exist once the annotations and the extraction projection have resolved, so a switch issued before that is bounced back to Study. Every sampled switch runs against a settled form.