Benchmark tools#

Audience: developers + performance-minded users
Time: 10–25 minutes
What you’ll learn:

  • How to run the built-in synthetic rendering benchmark

  • How to interpret the metrics (FPS, frame time, GPU memory, LOD)

  • How to use “Copy situation report” for bug reports and regressions

  • What this benchmark does not measure (and how to benchmark those separately)

Prerequisites:

  • Cellucid open in a current stable desktop release of Chrome, Edge, Firefox, or Safari with WebGL2 enabled


Where the benchmark lives (UI location)#

Open:

  • Left sidebar → Benchmark accordion

It includes:

  • six quick presets: 500K, 1M, 5M, 10M, 20M, 40M points,

  • a custom point count input accepting any whole number from 1 to 250,000,000,

  • eight synthetic data patterns (Pattern),

  • Embeddings: One, as generated, or Five: 3 in 3D, 2 in 2D (extra rotated, warped and projected copies of the generated cloud, each built when a view first shows it, as a dataset with several embeddings would be),

  • a Load synthetic data button,

  • a live metric readout that appears as soon as the panel is open,

  • a Copy situation report button,

  • and an Analyze performance tool that suggests what to change.

The Benchmark panel before any run: Synthetic data with six presets from 500K to 40M, Point count, Pattern and Embeddings, the Load synthetic data button, and Diagnostics with Analyze performance and Copy situation report; the live performance readout is omitted.

The panel before its first run. The live metric readout that sits between Load synthetic data and Copy situation report is left out of this picture on purpose: it would show the frame rate of the machine that took the screenshot. Its fields are listed in the table under What the panel reports below.#

Important

The synthetic benchmark loads synthetic data into the viewer.

Use it when:

  • you want to test rendering limits without needing a real dataset, or

  • you want to compare hardware/browser builds.

Do not use it to benchmark:

  • real dataset loading,

  • gene expression fetching,

  • analysis runtime,

  • or figure export.

For those, use Benchmarking methodology and metrics.


What this benchmark measures (and why it’s useful)#

This benchmark is primarily a rendering stress test:

  • how many points your GPU can render smoothly,

  • how well LOD/frustum culling helps,

  • and how GPU memory scales with point count.

It is especially useful for answering questions like:

  • “Can this laptop handle a 1M-cell dataset interactively?”

  • “Did a renderer change reduce FPS on the same machine?”

  • “Is the bottleneck likely GPU vs CPU?”


Quick start (3 minutes)#

  1. Open Benchmark in the left sidebar.

  2. Click a preset like 500K or 1M.

  3. Click Load synthetic data.

  4. Orbit/pan/zoom for ~10 seconds and watch:

    • FPS

    • Frame time

    • GPU memory

  5. If it feels slow, click Analyze performance and read the “What to do” list.

If you want a copy/paste summary for a bug report, click Copy situation report.



Data patterns (how to choose)#

The pattern dropdown controls the spatial distribution of synthetic points. The eight options, in the order they appear:

Pattern

What it produces

Use it to test

Model surface (default)

Points sampled over a loaded 3D model surface

A dense, structured shell — the pattern the acceptance samples used

Atlas-like

Many groups spread across the space

“Many groups across space” intuition

Batch effects

Offset duplicate structure

Overlapping-population intuition

Gaussian clusters

Compact blobs

The closest analogue to a clustered embedding

Uniform random

Points everywhere, no structure

The harshest case for LOD and frustum culling

Octopus

Radiating arms

Elongated, trajectory-like structure

3D spirals

Interleaved spiral arms

Fine structure that LOD can destroy visibly

Flat UMAP

A dense 2D manifold

The common 2D-embedding shape

If you are testing rendering limits, Uniform random is often the harshest, because nothing can be culled. If you are testing realistic single-cell scenes, Gaussian clusters or Atlas-like are more representative.

Generation is seeded, so the same pattern and count produce the same points on every run, and it happens on a worker thread — the interface stays responsive while a large set is built.


What the panel reports (every field, and its unit)#

The grid appears as soon as the panel is opened and measures whatever is loaded — your own dataset until a synthetic run replaces it. It shows six headline tiles, then a timing breakdown, and after a synthetic run the generation time.

Field

Unit / format

What it means

Points

count

How many synthetic points were published

FPS

whole frames per second

Derived from recent frame times

Frame time

X.XX ms, sometimes with a CPU Y.YY ms line under it

Wall time for a frame; the second line is the main-thread time spent issuing the last frame’s draw calls (not GPU time), shown only when the browser’s coarse timer resolves it above zero

GPU memory

N.N MB, or ~N.N MB (estimate)

Reported where the browser exposes it; otherwise estimated from the point count

LOD level

Full, or Level N

Which level of detail the renderer settled on

Visible points

count

How many points survived LOD and culling

Timing details → min / p95 / max

X.XX ms each

Frame-time distribution. Appears only once at least 30 frames have been sampled

Generated in … ms

whole ms

How long the synthetic set took to build

Important

The six headline tiles are CPU-side wall-clock measurements taken between frames. They are not GPU timer readings, so they include anything else the tab was doing.

Real GPU timing exists, but only inside Analyze performance, which uses the browser’s disjoint-timer-query extension where the driver provides it and falls back to CPU estimates where it does not. If you need to attribute time to the GPU specifically, that is the tool — not the headline FPS.

Reading them#

FPS and frame time

  • 60 FPS is the “smooth” target on most displays.

  • 16.7 ms/frame ≈ 60 FPS; 33 ms/frame ≈ 30 FPS.

  • If FPS drops as you increase point count: you are hitting a GPU limit.

  • If FPS is unstable with long spikes (high p95/max relative to min): you are seeing jank, which is usually a CPU or allocation problem rather than a drawing one.

GPU memory

  • It is the main predictor of “WebGL context lost” on large datasets.

  • If it climbs close to your GPU’s limit, expect context loss.

  • If memory is fine but FPS is low, you are pixel- or compute-bound, not memory-bound.

  • If the value is prefixed with ~, the browser did not expose a real figure and it is an estimate derived from the point count. Do not compare an estimate against a measured value.

LOD level and visible points

  • LOD exists to reduce the number of rendered points when zoomed out.

  • If LOD reduces Visible points and FPS improves, the renderer is adapting successfully.

  • If Visible points stays high and FPS stays low, try LOD in Visualization — and lower Force LOD level too when you need to go past Auto’s floor of min(2,000,000, cells ÷ 8) points, a smaller window, a smaller point size, or turning antialiasing off. Shader quality did not change frame time measurably on the hardware measured, in 2D or 3D. The reasons are in Performance considerations (what gets slow and why).

Related docs:


“Copy situation report” (use this for bug reports)#

The Copy situation report button is meant to produce a compact block of text you can paste into:

  • a GitHub issue,

  • a performance regression note,

  • or a lab internal thread.

Best practice:

  • include the report and your scenario description (“what I was doing when it was slow”).

If you’re filing an issue, follow Reporting performance bugs.


“Analyze performance” (why is it slow?)#

The Analyze performance button runs a short measurement and produces:

  • a Problems found list naming one primary bottleneck and any contributing ones,

  • a prioritized What to do checklist,

  • and a collapsed Detailed stats block with the per-phase overheads.

The bottleneck names it can print are GPU, CPU, Fragment/Fill Rate, Vertex Processing, LOD Selection, Frustum Culling, Frame Stuttering, JS/CPU Pressure, Garbage Collection and Main Thread Blocked. Use it as a triage assistant:

  • Fragment/Fill Rate — you are covering too many pixels. Reduce the point size first, then the window size, then the view count.

  • Vertex Processing — you are drawing too many points. Enable LOD, or filter.

  • LOD Selection / Frustum Culling — the selection work, not the drawing, is showing up. Avoid forcing a high LOD level.

  • CPU, JS/CPU Pressure, Main Thread Blocked — the frame is not GPU-limited at all; look at filtering, analysis and category accounting.

Note

Earlier builds printed a Shader Complexity bottleneck and a Shader overhead: figure, and suggested switching to Ultra-light. They are gone, and the detailed stats now report Point size response: in that row instead — the multiplier by which frame time grows as the point size is swept, computed from a sweep the analysis already performs.

The shader figure was removed rather than corrected because the instrument cannot support it. It timed the three qualities in a fixed order, one run each, for about sixteen frames apiece, and compared the spread against fixed thresholds. The effect itself is not there — the three qualities are within noise at every point size, because nearly every rasterised fragment dies at the depth test before the fragment shader runs. Adding counterbalancing and a measured noise floor was tried and still produced a false positive on a busy machine, so no threshold on that instrument is trustworthy.

Point size is the rendering setting that responds, and antialiasing is the largest single lever left on the plain path. Both are measured in Performance considerations (what gets slow and why). Running the analysis no longer changes your shader quality as a side effect.


Safety notes and footguns#

  • Synthetic benchmark data is not your real dataset. Reload the page to restore your original data.

  • Don’t start with 20M or 40M points on a laptop. Ramp up: 500K → 1M → 5M → 10M.

  • If you hit “WebGL context lost”, reduce point count and/or reduce GPU load (smaller window, no snapshots, no smoke).

  • The custom point count is capped at 250,000,000. A value outside 1–250,000,000 is rejected with a Point count out of range notice rather than attempted. Below it, no texture size limits the count: per-point textures are stacked in layers of MAX_TEXTURE_SIZE rows, so a GPU that reports 8,192 (Firefox) holds as many points as one that reports 16,384, and every draw is split into calls of at most 16,777,216 points, below the 30,000,000 vertices Firefox accepts in one call. No GPU buffer limits it either: positions and colours are stored in segments of 16,777,216 points, one buffer each (one buffer of 3D positions used to stop at 178.8 million points). Nor does the browser’s largest single array: Chromium allocates at most 2,145,386,496 bytes in one ArrayBuffer (3D positions of 178.8 million points; WebKit 4 GiB, Firefox 16 GiB), so positions it refuses are made in a WebAssembly memory instead, which holds up to 4 GiB on all three engines (357.9 million 3D points). The worker generates them there and sends them to the page in pieces of 192 MiB, which the page puts back together in one memory of its own. Measured in Chromium: 178,956,971 3D points (2^29 + 1 floats) generated, assembled and handed to the tree worker in 11 s, the tree built in 50 s.

  • What you will meet before that is memory. Measured in Chromium on an Apple M1 Pro (16 GB), as the physical memory of the tab’s renderer and GPU processes together: 100,000,000 3D points take 5.4 GB once drawn, 7.8 GB as their levels of detail are taken in and 5.0 GB after (50 bytes a point); 100,000,000 flatumap (2D) points take 4.3 GB once drawn and 5.3 GB after (53 bytes a point), and 150,000,000 take 6.0 GB once drawn, 6.8 GB as their levels of detail are taken in and 6.5 GB after. Past about 175 million 3D points the whole app would need about 9 GB steady and 14 GB while its levels of detail are built, more than a 16 GB machine holds. On a machine that cannot allocate it the request fails as an allocation error, which is the honest outcome — the limit that binds is your machine’s memory, and the control does not pretend to know what that is.

  • The controls in this panel start out disabled and become usable once the app has finished wiring them. If a preset looks greyed out for a moment during a slow start, that is the intended behaviour — see Troubleshooting (Web App): Start Here.


Next steps#