Skip to content

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:

Gate100k1m
Completed CPU-inclusive creation p955,000 ms45,000 ms
Completed resize p952,000 ms20,000 ms
Synchronous pointer / keyboard inspection p9550 ms100 ms
Retained incremental JS heap512 MiB4,096 MiB
CPU GPU-submission p9533 ms100 ms
Vertex buffer storage16 MiB128 MiB
Dense-bar floating coverage storage at reference DPR 132 MiB64 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.

Terminal window
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 --check

GPU 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.

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 familyCreate p95 before → after msObserved changeRetained JS before → after MiBObserved changeResize p95 before → after ms
groupedColumn28,618.6 → 20,018.2-30.1%2,104.9 → 1,436.1-31.8%15,835.2 → 8,203.4
groupedBar31,622.4 → 21,003.2-33.6%2,134.8 → 1,467.5-31.3%17,869.0 → 11,938.3
scatter50,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.

FamilyInputsCreate p95 msResize p95 msJS MiBPointer / key p95 msSubmit p95 msVertex / coverage MiBGates
groupedColumn1000001522.9485.1145.90.2 / 0.62.61.5 / 6.4pass
groupedColumn100000020018.28203.41436.11.4 / 2.0108.915.3 / 6.4submit
groupedBar1000001350.2613.0149.10.2 / 0.61.51.5 / 6.4pass
groupedBar100000021003.211938.31467.52.3 / 1.6103.015.3 / 6.4submit
scatter1000001890.0521.1214.20.1 / 0.51.21.5 / 0.0pass
scatter100000031630.215282.62120.70.9 / 2.3124.215.3 / 0.0submit
heatmap100000702.0128.986.20.0 / 0.50.40.1 / 0.0pass
heatmap10000008023.02068.9820.00.0 / 0.46.10.1 / 0.0pass

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.

Terminal window
node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-memory-optimized.json --check
# Expected nonzero: three remaining 1m submission-budget misses.

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 familyCPU / GPU create p95 msCPU / GPU resize p95 msCPU / GPU retained JS MiB
groupedColumn26,866.5 / 20,018.214,521.6 / 8,203.41,435.9 / 1,436.1
groupedBar23,905.8 / 21,003.211,175.3 / 11,938.31,467.3 / 1,467.5
scatter28,524.2 / 31,630.214,622.8 / 15,282.62,120.4 / 2,120.7
heatmap7,658.6 / 8,023.02,089.4 / 2,068.9823.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.

Terminal window
node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-memory-cpu.json --check

Functional 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 groupedBar submission p95: 96.7 ms, budget 100 ms (previously 110.3 ms).
  • 1m scatter completed creation p95: 38,406.7 ms, budget 45,000 ms (previously 50,546.3 ms).
FamilyInputsCreate p95 msResize p95 msJS MiBPointer / key p95 msSubmit p95 msVertex / coverage MiBGates
groupedBar1000001912.2635.4218.60.1 / 0.51.41.5 / 6.4pass
groupedBar100000026510.710717.92134.80.8 / 1.396.715.3 / 6.4pass
groupedColumn1000001722.6451.6215.10.2 / 0.52.91.5 / 6.4pass
groupedColumn100000023329.28437.52105.41.0 / 1.697.815.3 / 6.4pass
heatmap100000957.599.6115.00.0 / 0.40.60.1 / 0.0pass
heatmap100000011535.91627.71107.40.0 / 0.51.00.1 / 0.0pass
scatter1000002743.6487.1296.50.1 / 0.71.21.5 / 0.0pass
scatter100000038406.79419.02935.80.4 / 0.982.115.3 / 0.0pass

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.

Terminal window
node packages/charts/scripts/report-webgl.mjs packages/charts/docs/webgl-optimized.json --check

This 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.

FamilyInputsSVG / WebGL create p95 msSVG / WebGL resize p95 msSVG / WebGL retained JS MiB
groupedBar1000001897.0 / 1912.2624.2 / 635.4218.4 / 218.6
groupedBar100000025546.2 / 26510.79709.7 / 10717.92134.5 / 2134.8
groupedColumn1000001673.3 / 1722.6448.0 / 451.6214.9 / 215.1
groupedColumn100000022547.8 / 23329.28286.3 / 8437.52104.6 / 2105.4
heatmap100000939.2 / 957.581.3 / 99.6114.5 / 115.0
heatmap100000011399.3 / 11535.91625.2 / 1627.71107.0 / 1107.4
scatter1000002679.0 / 2743.6473.4 / 487.1296.3 / 296.5
scatter100000035538.1 / 38406.79527.8 / 9419.02935.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.