WebGL performance protocol and budgets
Latest result: the 19 October CPU/memory optimization reduces measured 1m creation and retained JS for all three selected families. Three 1m GPU-submission budget misses remain; the full GPU performance gate is open. Earlier closure tables are historical.
Reference: Apple M1 Pro, MacBookPro18,3, 16 GiB RAM, macOS 26.4 (25E5207k),
Chromium 147.0.7727.15, full chromium channel, ANGLE Metal / Apple M1 Pro.
Both CPU baseline and GPU use --use-angle=metal and an explicit 8 GiB V8 heap
ceiling (--js-flags=--max-old-space-size=8192). This is a stress-test launch
configuration, not a claim that default browser heap limits accept 1m inputs.
The retained incremental JS budget remains 4 GiB. Software SwiftShader exploratory
runs are excluded from hardware performance claims.
The 19 October CPU/memory optimization targets a reduction of at least 25% in
both completed creation p95 and retained incremental JS heap for each 1m
groupedColumn, groupedBar and scatter case, compared with fresh, unprofiled
reference triples on the same machine and production graph before optimization.
All existing absolute gates below remain unchanged. The 100k, CPU SVG, 1k
default-SVG and functional compatibility checks remain regression requirements.
CPU and sampled-allocation profiles are diagnostic runs and are excluded from
performance gate claims.
Budgets were recorded before GPU implementation in the local feature plan:
| Gate | 100k | 1m |
|---|---|---|
| Completed CPU-inclusive creation p95 | 5,000 ms | 45,000 ms |
| Completed resize p95 | 2,000 ms | 20,000 ms |
| Synchronous pointer / keyboard inspection p95 | 50 ms | 100 ms |
| Retained incremental JS heap | 512 MiB | 4,096 MiB |
| CPU GPU-submission p95 | 33 ms | 100 ms |
| Vertex buffer storage | 16 MiB | 128 MiB |
| Dense-bar floating coverage storage at reference DPR 1 | 32 MiB | 64 MiB |
Acceptance protocol: three serial standalone production-ESM runs per case, with no concurrent build/test work. Execution notes identify runs where that isolation was not established. Pivot has separate functional compatibility coverage; this is not a Pivot host performance measurement.
Nearest-rank p95 of three is their maximum. The 960×440 host resizes to 800×440.
Create includes model normalization/validation/snapshots, CPU geometry, semantic
index, CPU fallback paths, GPU upload/draw, two RAFs and a benchmark-only
gl.finish() completion fence. Module/data construction precedes the timer.
Resize waits 100 ms for observation and two RAFs (the fixed 100 ms is excluded),
then finishes GPU work. Ordinary interactive paints never call finish().
CPU baselines use batched SVG for bars/scatter and native SVG for heatmap under the same fixture/launch protocol. Their values are comparable within this matrix; the original M2 protocol is retained separately.
Bar inputs are one value per row; scatter has one numeric X/Y pair per row (two analytical measures). Heatmap counts are matrix cells: 1,000 series × 100/1,000 rows. Existing bounded row/column reduction is retained, so heatmap paints at most 2,400 buckets, rather than 100k/1m raw GPU marks. Functional fixtures separately verify single-source raw activation and bucket semantics. No nulls are used in timing fixtures; functional tests cover nulls/clipping.
Each case pools 60 synthetic synchronous pointer/key measurements; these exclude tooltip delay, browser input scheduling and next-frame presentation. Pointer measurements include canonical CPU hit/description resolution. Heatmap aggregate bucket measurements are inspection, not activation of an invented raw record. Real input activation has separate browser coverage. These measurements do not establish smooth panning, 60fps or generalized interaction-latency parity. Export latency is not benchmarked.
CDP GC precedes/follows each run. Retained JS includes the committed CPU model, index and scene; raw records also include embedder/backing storage, node/batch counts, actual driver, shape dimensions, device pixel ratio and GPU byte metrics. The tracked vertex-buffer metric is retained allocated capacity of the shared upload buffer. Uniform-paint/shape plots use 16 bytes per instance with exact constant shape and color attributes. Mixed paints/shapes expand once to 64 bytes per instance, retaining one instanced draw per plot and source-over order even for alternating series. Vertex capacity grows when needed and is reused for smaller frames. Uniform CPU typed staging is also reused by plot index, avoiding fresh geometric growth on resize. Unused staging is discarded after collection; destroy clears it, and context loss retains only the live CPU geometry needed for recovery. Floating coverage is width × height × 16 bytes. Drawing color storage is width × height × 4 an estimate, not a GPU-process allocation measurement. Preserved drawing buffers, driver copies, native process RSS, OS/GPU allocations and long-session memory are not measured. Do not add these estimates to JS heap and present them as total GPU/application memory. GPU-submission time measures CPU work in the draw/upload loop (typed view setup, upload and draw calls), not GPU execution alone. Earlier canonical geometry collection/staging and resource initialization are outside this narrow loop; completed create/resize timings include them. The timing boundaries remain the same between the initial and final matrices. Typed staging removes the duplicate JS-number scene and late Float32 copy. The closure keeps the same timer and budgets; attribute setup now also occurs inside the measured upload/draw loop. Assertion-only validation checks mapped metadata without constructing discarded owned frames; public parsing still owns every snapshot. Derived/hierarchy frames retain their existing validation/materialization owner. Scatter point collection and lookup avoid temporary per-observation arrays and string tuple keys. Floating coverage textures also retain capacity on shrink; texture-coordinate scaling samples only the current viewport. Completeness is checked on allocation or growth and again after recovery, avoiding a synchronous framebuffer check on every redraw. The existing coverage kernel is unchanged; resize and DPR 1/2 fixtures verify the resulting pixels.
Reproduction
Section titled “Reproduction”E2E_PACKAGE=charts E2E_PORT=5421 CHART_GPU_BACKEND=metal CHART_WEBGL_BENCHMARK=/private/tmp/charts-gpu pnpm --filter @revolist/revogrid-e2e-enterprise exec playwright test tests/chart-webgl-benchmark.spec.ts --config=playwright.webgl.config.ts --project=charts --workers=1# Repeat serially with CHART_WEBGL_BASELINE=1 and a different output prefix.node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-optimized.json --checkGPU fallback is an error in this benchmark, never a GPU pass. Partial cases, timeouts and crashes remain explicitly incomplete. Timing-fixture completion is separate from satisfying the performance gates.
For diagnostics only, set CHART_WEBGL_PROFILE=/private/tmp/chart-cpu and/or
CHART_WEBGL_HEAP_PROFILE=/private/tmp/chart-heap. The first run records a CDP
CPU profile and/or a retained-allocation profile sampled at 1 MiB intervals.
The report checker rejects these instrumented matrices. callerDatasetRetained
records whether the input dataset remains reachable through a benchmark-only
WeakRef after the evaluation returns and CDP GC completes. This checks internal
retention; an application can still intentionally retain its own input data.
CPU and retained-memory optimization — 19 October 2026
Section titled “CPU and retained-memory optimization — 19 October 2026”The latest implementation removes redundant work without reducing the number of observations, canonical inspection entries or CPU fallback paths:
- Ordered one-cell-per-observation frames validate and clone positionally, avoiding several full-size lookup maps. Reordered/shared mappings use the general parser.
- Standalone runtime closures retain callback references rather than the caller’s complete options/frame. Unobserved change notifications avoid an unused snapshot; public getters, subscribers and custom-renderer inputs retain independent ownership.
- Only a freshly parsed native-renderer snapshot can reuse the private validation proof. Direct, decorated and custom renderer calls retain their validation path.
- Grouped bars reuse value rows and resolve inspection lazily instead of allocating per-category maps and stacked endpoint matrices. Contiguous X coordinates use positional lookup; reordered numeric windows keep explicit maps.
- Batched inspection uses numeric spatial buckets and ordered source arrays with binary lookup, retaining a map fallback for unordered collectors.
- Scatter retains group references rather than copied group fields, releases its temporary point collection after paint, resolves inspection from the canonical matrix, and computes named-axis extrema without million-element filter/map arrays.
Diagnostic CPU profiles identified GC, snapshot cloning and metadata lookup/index construction as the dominant CPU costs. Sampled allocation profiles and a post-GC WeakRef check confirmed that the original caller dataset was retained before these changes and released afterward. An application can still retain its own input. The chart continues to retain owned model/render data and a semantic inspection index; this is not a zero-copy or constant-memory implementation.
The fresh reference matrix (packages/charts/docs/webgl-memory-reference.json in the source repository) records the pre-change
production graph. The contended final-graph matrix (packages/charts/docs/webgl-memory-contended.json in the source repository)
retains all eight completed cases run while unrelated Node jobs competed for CPU;
it misses the 1m GPU-submission gates and is not quiet-machine acceptance evidence.
The 18 October results below remain historical and do not override newer misses.
The reference task had no acknowledged quiet window, so CPU before/after percentages
are local observed differences, not a controlled paired speedup. The optimized
acceptance run uses the separately coordinated quiet window. Every matrix
identifies its emitted JavaScript hash. Instrumented profiles are
excluded from gate checks. Retained JS is measured after GC, not peak memory,
native process RSS, total application memory or GPU-driver allocation.
Latest measured GPU matrix
Section titled “Latest measured GPU matrix”The final graph was measured between 11:39:52 and 11:47:03 UTC, inside the coordinated 11:39–11:57 UTC hold on other workspace builds/tests (extended to finish CPU/SVG controls). All eight timing fixtures complete. Each selected 1m case exceeds the observed 25% creation/retained-JS reduction target, subject to the reference-isolation limitation above.
| 1m family | Create p95 before → after ms | Observed change | Retained JS before → after MiB | Observed change | Resize p95 before → after ms |
|---|---|---|---|---|---|
| groupedColumn | 28,618.6 → 20,018.2 | -30.1% | 2,104.9 → 1,436.1 | -31.8% | 15,835.2 → 8,203.4 |
| groupedBar | 31,622.4 → 21,003.2 | -33.6% | 2,134.8 → 1,467.5 | -31.3% | 17,869.0 → 11,938.3 |
| scatter | 50,021.1 → 31,630.2 | -36.8% | 2,939.7 → 2,120.7 | -27.9% | 11,884.8 → 15,282.6 |
Scatter resize is slower than the fresh reference (15,282.6 vs 11,884.8 ms), although below its 20,000 ms absolute gate. This change does not claim a scatter resize improvement. All creation, resize, inspection, retained-JS, vertex and coverage gates pass. The 1m GPU-submission gates remain open for groupedColumn (108.9 ms), groupedBar (103.0 ms) and scatter (124.2 ms), against 100 ms. Therefore the full GPU performance checker intentionally exits nonzero; these results are an improvement, not closure of all M3 performance requirements.
| Family | Inputs | Create p95 ms | Resize p95 ms | JS MiB | Pointer / key p95 ms | Submit p95 ms | Vertex / coverage MiB | Gates |
|---|---|---|---|---|---|---|---|---|
| groupedColumn | 100000 | 1522.9 | 485.1 | 145.9 | 0.2 / 0.6 | 2.6 | 1.5 / 6.4 | pass |
| groupedColumn | 1000000 | 20018.2 | 8203.4 | 1436.1 | 1.4 / 2.0 | 108.9 | 15.3 / 6.4 | submit |
| groupedBar | 100000 | 1350.2 | 613.0 | 149.1 | 0.2 / 0.6 | 1.5 | 1.5 / 6.4 | pass |
| groupedBar | 1000000 | 21003.2 | 11938.3 | 1467.5 | 2.3 / 1.6 | 103.0 | 15.3 / 6.4 | submit |
| scatter | 100000 | 1890.0 | 521.1 | 214.2 | 0.1 / 0.5 | 1.2 | 1.5 / 0.0 | pass |
| scatter | 1000000 | 31630.2 | 15282.6 | 2120.7 | 0.9 / 2.3 | 124.2 | 15.3 / 0.0 | submit |
| heatmap | 100000 | 702.0 | 128.9 | 86.2 | 0.0 / 0.5 | 0.4 | 0.1 / 0.0 | pass |
| heatmap | 1000000 | 8023.0 | 2068.9 | 820.0 | 0.0 / 0.4 | 6.1 | 0.1 / 0.0 | pass |
Raw optimized GPU matrix (packages/charts/docs/webgl-memory-optimized.json in the source repository) preserves every run,
including all outliers, the confirmed hardware driver and caller-frame reachability
checks. Caller datasets are released in all optimized runs. GPU byte counts and
DOM/batch counts retain the same full-observation geometry contract.
node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-memory-optimized.json --check# Expected nonzero: three remaining 1m submission-budget misses.Matched CPU comparison
Section titled “Matched CPU comparison”All eight CPU baseline cases complete and pass their applicable unchanged gates under the same full-Chromium/Metal/8 GiB heap protocol. Bars/scatter use batched SVG; heatmap uses native SVG and its existing bounded bucket reduction.
| 1m family | CPU / GPU create p95 ms | CPU / GPU resize p95 ms | CPU / GPU retained JS MiB |
|---|---|---|---|
| groupedColumn | 26,866.5 / 20,018.2 | 14,521.6 / 8,203.4 | 1,435.9 / 1,436.1 |
| groupedBar | 23,905.8 / 21,003.2 | 11,175.3 / 11,938.3 | 1,467.3 / 1,467.5 |
| scatter | 28,524.2 / 31,630.2 | 14,622.8 / 15,282.6 | 2,120.4 / 2,120.7 |
| heatmap | 7,658.6 / 8,023.0 | 2,089.4 / 2,068.9 | 823.6 / 820.0 |
Raw matched CPU matrix (packages/charts/docs/webgl-memory-cpu.json in the source repository) includes 100k and 1m triples.
GPU mode does not consistently beat CPU mode: scatter creation is slower in this
GPU matrix, and canonical parsing, snapshots, domains, inspection and fallback
paths remain CPU work. The retained-JS figures remain similar; GPU storage is a
separate metric. These results do not establish general GPU/SVG speed equivalence.
The original M2 protocol and 1k single-item SVG comparison are reported separately
in batched SVG performance.
node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-memory-cpu.json --checkFunctional verification for this change: 3,388 Charts unit tests; 65 Chromium browser cases covering standalone/Pivot raw inspection, named bar/relationship axes, navigation, context loss/recovery, unavailable-GPU fallback, DPR/resize, default/opt-out transitions, exports and package consumers; 18 Firefox/WebKit SVG/fallback cases. Charts and Pivot public-type checks, the Charts production build and isolated package-consumer checks pass. Firefox/WebKit cases do not establish hardware WebGL performance on those browsers.
Historical GPU results — 18 October 2026 closure
Section titled “Historical GPU results — 18 October 2026 closure”All eight workloads complete on real Metal hardware and meet every unchanged reference gate. The two selected misses are closed:
- 1m
groupedBarsubmission p95: 96.7 ms, budget 100 ms (previously 110.3 ms). - 1m
scattercompleted creation p95: 38,406.7 ms, budget 45,000 ms (previously 50,546.3 ms).
| Family | Inputs | Create p95 ms | Resize p95 ms | JS MiB | Pointer / key p95 ms | Submit p95 ms | Vertex / coverage MiB | Gates |
|---|---|---|---|---|---|---|---|---|
| groupedBar | 100000 | 1912.2 | 635.4 | 218.6 | 0.1 / 0.5 | 1.4 | 1.5 / 6.4 | pass |
| groupedBar | 1000000 | 26510.7 | 10717.9 | 2134.8 | 0.8 / 1.3 | 96.7 | 15.3 / 6.4 | pass |
| groupedColumn | 100000 | 1722.6 | 451.6 | 215.1 | 0.2 / 0.5 | 2.9 | 1.5 / 6.4 | pass |
| groupedColumn | 1000000 | 23329.2 | 8437.5 | 2105.4 | 1.0 / 1.6 | 97.8 | 15.3 / 6.4 | pass |
| heatmap | 100000 | 957.5 | 99.6 | 115.0 | 0.0 / 0.4 | 0.6 | 0.1 / 0.0 | pass |
| heatmap | 1000000 | 11535.9 | 1627.7 | 1107.4 | 0.0 / 0.5 | 1.0 | 0.1 / 0.0 | pass |
| scatter | 100000 | 2743.6 | 487.1 | 296.5 | 0.1 / 0.7 | 1.2 | 1.5 / 0.0 | pass |
| scatter | 1000000 | 38406.7 | 9419.0 | 2935.8 | 0.4 / 0.9 | 82.1 | 15.3 / 0.0 | pass |
Raw GPU closure matrix (packages/charts/docs/webgl-optimized.json in the source repository) preserves all three runs per case,
actual driver, fixture shape and emitted-JavaScript hash. The previous integrated
matrix (packages/charts/docs/webgl-measured.json in the source repository) retains the two misses; the initial hardware
matrix (packages/charts/docs/webgl-initial.json in the source repository) retains earlier submission and argument-limit failures.
Profiled, interrupted and experimental runs are excluded from these closure
claims. No observations, inspection entries or CPU fallback paths were removed.
node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-optimized.json --checkThis is one named machine/browser and three runs per case, not cross-host or cross-driver performance parity. The public styling/interaction capability matrix is unchanged; passing these gates does not establish SVG feature equivalence, generalized 60fps navigation or performance for other chart families.
Historical matched CPU SVG comparison — 18 October 2026
Section titled “Historical matched CPU SVG comparison — 18 October 2026”The CPU matrix uses the same production graph, fixtures, launch flags and serial
protocol. Raw CPU reference (packages/charts/docs/webgl-optimized-cpu-baseline.json in the source repository) preserves every
run. Heatmap bucket reduction and explicit source limits are identical. SVG uses
the browser’s normal compositor; hardware acceleration is not disabled.
| Family | Inputs | SVG / WebGL create p95 ms | SVG / WebGL resize p95 ms | SVG / WebGL retained JS MiB |
|---|---|---|---|---|
| groupedBar | 100000 | 1897.0 / 1912.2 | 624.2 / 635.4 | 218.4 / 218.6 |
| groupedBar | 1000000 | 25546.2 / 26510.7 | 9709.7 / 10717.9 | 2134.5 / 2134.8 |
| groupedColumn | 100000 | 1673.3 / 1722.6 | 448.0 / 451.6 | 214.9 / 215.1 |
| groupedColumn | 1000000 | 22547.8 / 23329.2 | 8286.3 / 8437.5 | 2104.6 / 2105.4 |
| heatmap | 100000 | 939.2 / 957.5 | 81.3 / 99.6 | 114.5 / 115.0 |
| heatmap | 1000000 | 11399.3 / 11535.9 | 1625.2 / 1627.7 | 1107.0 / 1107.4 |
| scatter | 100000 | 2679.0 / 2743.6 | 473.4 / 487.1 | 296.3 / 296.5 |
| scatter | 1000000 | 35538.1 / 38406.7 | 9527.8 / 9419.0 | 2935.6 / 2935.8 |
Both policies meet the common creation, resize, synchronous inspection, retained JS and structural gates in this reference matrix. GPU-only submission and buffer gates do not apply to SVG. The CPU model, semantic index and retained SVG fallback remain authoritative and contribute substantially to creation/resize. These results do not establish a general GPU speedup. Native/process memory, peak GPU allocation and long-session retention are not measured.
The previous CPU matrix (packages/charts/docs/webgl-cpu-baseline.json in the source repository) is retained for comparison.
M2’s separate SVG and 1k default-renderer gates are rechecked on this same graph;
see batched SVG verification.