Skip to content

perf(desktop): Engine Benchmark plate crops decode via the engine codec, not pure-Dart - #545

Merged
badbread merged 2 commits into
mainfrom
fix/ab-benchmark-plate-crop-decode
Aug 7, 2026
Merged

perf(desktop): Engine Benchmark plate crops decode via the engine codec, not pure-Dart#545
badbread merged 2 commits into
mainfrom
fix/ab-benchmark-plate-crop-decode

Conversation

@badbread

@badbread badbread commented Aug 7, 2026

Copy link
Copy Markdown
Owner

The client was finally measured, and the bottleneck is the pure-Dart decode

#391's correction said to start by measuring the client, not the server. Done: a harness that mounts the real benchmark dialog (widget test against an in-process HTTP server serving a 165-pass report + realistic ~150-175 KB 1080p JPEG frames), plus micro-benchmarks of the exact plate_crop.dart pipeline. All numbers from the same 8-core Windows build machine, Flutter 3.44.6.

What the dialog actually does (measured, not assumed)

  • The pass list is a lazy ListView.separated: only 7 rows mount at the default dialog size, 10 snapshot fetches go out (visible + cache extent) — there is no 165-row build and no 165-request stampede. Two of the issue's three candidates are refuted.
  • The report fetch and dialog build are fast (3 ms server-side per Engine Benchmark: pass images load slowly (cause NOT yet identified) #391's table; first render a few hundred ms in the harness including test overhead).
  • What remains, per newly-mounted row: fetch the full frame, then package:image's pure-Dart JPEG decode of that frame to derive the plate crop, in a fresh isolate. That decode is the bottleneck.

Where the time goes (before)

operation (per image) time
fetch full frame, loopback + 8 ms simulated LAN ~10-70 ms (43 ms wall for 10 concurrent)
img.decodeImage, 1080p ~130 ms
img.decodeImage, 4 MP ~262 ms
copyCrop + encodeJpg (the rest of the crop) ~5-14 ms
engine (dart:ui) decode of the same 1080p bytes 18 ms
engine decode at thumbnail size (cacheWidth) 5 ms
Isolate.run spawn overhead ~1 ms

A 10-row screenful, current pipeline: 478 ms wall — of which fetch-only is 43 ms. ~90% of image latency is the client decode, paid again for every screenful scrolled.

Change

cropPlateToBbox now decodes with the engine's native codec (instantiateImageCodecWithSize) and rasterizes only the bbox region (drawImageRecttoImage → small readback), so the full decoded frame never crosses into Dart. package:image still handles the near-black guard and the JPEG re-encode, but only ever sees the tiny crop. The pure-Dart path is kept unchanged as the reference implementation and as the fallback when the engine can't decode the bytes.

Callers are untouched: same signature, same JPEG bytes out, same (bytes, w, h) dimensions/aspect contract, same null-on-near-black semantics (issue #179), same crop cache. The Plates gallery/detail crops and the PDF report ride the same helper, so they get the same win.

After (same harness, same machine)

measurement before after
single crop, 1080p (cropPlateToBbox) 141 ms 32 ms
12 concurrent crops 429 ms 156 ms
10-row screenful (fetch + crop wall) 478 ms 156 ms
crop cost per row (median, concurrent) 292 ms 52 ms

The 43 ms fetch floor is unchanged; thumbs now land ~3x sooner per screenful, and the per-row cost no longer scales with camera resolution nearly as hard (the 4 MP pure-Dart decode was 2x the 1080p one; native decode is a fraction of either).

Tests

  • New test/plate_crop_region_test.dart locks the fast path: crops the correct region at the correct dimensions (red-box frame), agrees with the pure-Dart reference on clamped dimensions for an out-of-range box, still rejects a near-black crop, and degrades (not throws) on undecodable bytes.
  • Existing plate_crop_aspect_test.dart passes unchanged on the new path.
  • flutter analyze: no new issues (224 pre-existing infos before and after).
  • flutter test: 157/157 pass (153 existing + 4 new).

On #394 (closed server-side derivatives)

Nothing here changes that verdict. With the bottleneck confirmed client-side, the derivatives were solving the wrong problem for LAN use; they remain a legitimate future win for VPN/cellular where 148 KB/row is real transfer cost, and would compose fine with this change if revived.

Fixes #391

The Engine Benchmark's measured client-side bottleneck (#391) was
package:image's pure-Dart JPEG decode of the full detection frame, run per
newly-mounted row to derive the plate crop: ~130 ms per 1080p frame (~260 ms
at 4 MP) against ~18 ms for the engine's native codec. A 10-row screenful
spent ~478 ms wall on images, of which only ~43 ms was fetch.

cropPlateToBbox now decodes with the engine codec and rasterizes only the
bbox region, so the full decoded frame never crosses into Dart; package:image
touches just the small crop (near-black guard + JPEG re-encode). The pure-Dart
path stays as the reference implementation and decode fallback. Same 10-row
screenful now: ~156 ms wall. Crop bytes, dimensions, aspect caching, and the
near-black rejection semantics are unchanged (locked by the new region tests).

Signed-off-by: badbread <badbread@users.noreply.github.com>
Signed-off-by: badbread <badbread@users.noreply.github.com>
@badbread
badbread merged commit 6f98124 into main Aug 7, 2026
7 checks passed
@badbread
badbread deleted the fix/ab-benchmark-plate-crop-decode branch August 7, 2026 18:33
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.

Engine Benchmark: pass images load slowly (cause NOT yet identified)

1 participant