Skip to content

Improve screenshot pixel flipping with contiguous row copies - #5852

Open
diarmidmackenzie wants to merge 1 commit into
aframevr:masterfrom
diarmidmackenzie:screenshot-performance-fix
Open

Improve screenshot pixel flipping with contiguous row copies#5852
diarmidmackenzie wants to merge 1 commit into
aframevr:masterfrom
diarmidmackenzie:screenshot-performance-fix

Conversation

@diarmidmackenzie

Copy link
Copy Markdown
Contributor

(PR assisted by Codex/Astra)

Description:

Improve screenshot performance by copying contiguous pixel rows instead of traversing the image column by column. This avoids the cache-unfriendly memory access pattern that makes large screenshots particularly slow, while preserving the synchronous getCanvas() API.

Changes proposed:

  • Use typed-array subarray() and set() to copy rows in reverse order.
  • Allocate the output buffer without first copying pixels that will all be overwritten.
  • Add two small, GPU-independent tests covering odd/even heights, RGBA preservation, and unchanged source pixels.
  • Update existing screenshot tests to wait for their asynchronous assertions.

CPU performance

Local benchmarks comparing the implementation on master with this change:

Resolution Original flip Row copy Speedup
128×64 0.058 ms 0.029 ms 2.0×
256×128 0.245 ms 0.057 ms 4.3×
512×256 1.023 ms 0.147 ms 7.0×
1024×512 5.912 ms 0.536 ms 11.0×
1920×1080 20.577 ms 2.811 ms 7.3×
2048×1024 62.503 ms 2.049 ms 30.5×
4096×2048 332.818 ms 10.863 ms 30.6×
8192×4096 1501.262 ms 40.097 ms 37.4×

Measured on an Intel i5-1035G1 using Node.js v22.22.2. Results are medians of nine batches after warmup, alternating implementation order, with multiple calls per batch at smaller resolutions. Timings include output allocation but exclude rendering, readback, canvas writing, and PNG encoding.

The absolute saving is small at low resolutions, but reaches about 322 ms at the default 4096×2048 screenshot size.

Browser-native alternative explored

Following this suggestion on #5789, we also explored createImageBitmap(imageData, {imageOrientation: 'flipY'}) followed by transfer to a bitmaprenderer canvas.

The comparison below includes buffer conversion and canvas writing for both paths, so these timings should not be compared directly with the flip-only results above.

Resolution Optimized CPU → canvas Browser-native flip → canvas
256×128 0.3 ms 0.2 ms
512×256 0.9 ms 0.7 ms
1024×512 2.8 ms 3.9 ms
1920×1080 12.0 ms 14.5 ms
2048×1024 11.9 ms 13.8 ms
4096×2048 59.7 ms 70.1 ms
8192×4096 268.1 ms 248.2 ms

Measured in ChromeHeadless 152 on the same machine: medians of five measurements after two warmups, alternating order. The flip-orientation probe passed.

A separate run extended the comparison to larger images, releasing each output canvas between measurements to limit memory use:

Resolution Optimized CPU → canvas Browser-native flip → canvas Time saved
8192×4096 260 ms 210 ms 19%
12288×6144 547 ms 432 ms 21%
16384×8192 1086 ms 811 ms 25%

These are medians of three measurements after one warmup. Rendering, readback, and PNG encoding are excluded. Hardware acceleration was not verified, and main-thread responsiveness was not measured.

The browser-native approach provides a modest additional improvement at very large sizes, but would introduce asynchronous capture behavior, capability detection and fallback handling, canvas context management, and additional testing. Changing getCanvas() to return a Promise would also affect existing callers.

This PR therefore keeps the simpler CPU optimization, which delivers most of the measured improvement without changing the public API. An asynchronous capture API could be investigated separately if responsiveness during very large captures remains a problem.

Validation

  • All five screenshot tests pass in ChromeHeadless.
  • Targeted lint and git diff --check pass.
  • The new unit tests use tiny typed arrays and require no GPU; performance benchmarks are separate from the unit suite.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant