Skip to content

refactor(inference): reuse the displayed asset for browser pixel preparation - #874

Merged
JArmandoAnaya merged 6 commits into
mainfrom
feat/browser-inference-asset-reuse
Sep 17, 2026
Merged

JArmandoAnaya merged 6 commits into
mainfrom
feat/browser-inference-asset-reuse

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

Summary

  • Retains the exact fetched Blob behind AssetImage's single object URL, so the visible <img> stays the one and only decode of a displayed asset.
  • Adds a generation-scoped DecodedAssetImage seam (AnnotatorCanvas's new onImageReady) that hands back the exact rendered <img> once loaded, with lazy, on-demand RGB extraction (readRgb) drawn at the descriptor's frame — never the natural one — and never invoked during ordinary rendering.
  • Invalidates outstanding leases both when imageSrc changes on a live instance and when the component unmounts (the path every real host actually takes on an asset switch).
  • Documents the measured browser-canvas-vs-Pillow decode divergences (alpha premultiplication, EXIF orientation, ICC profile handling) without claiming byte parity where none was verified.
  • Proves the whole seam in a real Chromium run (e2e/assetPixels.spec.ts) against a runtime-encoded PNG, behind /demo?scene=asset-pixels — the same test-only-route pattern the existing benchmark already uses, so no dedicated production route exists purely to host a Playwright harness.
  • No dependency is added between @visionset/annotator and @visionset/browser-inference in either direction; no model, geometry, or inference capability is introduced by this phase.

Test plan

  • pnpm --filter @visionset/annotator test — 1960/1960 passing
  • pnpm --filter @visionset/annotator lint
  • pnpm --filter @visionset/annotator build
  • pnpm --filter @visionset/ui-core test — 1393/1393 passing
  • pnpm --filter @visionset/ui-core lint
  • pnpm --filter @visionset/ui-core build
  • bash scripts/verify_npm_packages.sh
  • pnpm test:scripts — 153/153 passing
  • bash scripts/check.sh — all groups (python, frontend, generated, browser) passed
  • bash scripts/check.sh docs
  • pnpm exec playwright test e2e/assetPixels.spec.ts — 1 passed, real Chromium, real dev server

Known limitation for a future phase

AnnotatorCanvas's onImageReady guard cannot, by itself, distinguish a delayed load event from a fully live-instance imageSrc change from a legitimate one, because React always invokes the most recently committed handler regardless of when the underlying event was queued. No host exercises this today — every real asset switch unmounts the component (covered separately) — and browsers do not dispatch load/error for an abandoned image request in the first place. Documented in AnnotatorCanvas.tsx beside the guard and the onImageReady prop for whichever future phase adds the first live-instance consumer.

…y browser pixel source

AssetImage retains the exact fetched Blob behind its object URL (one
content request, one Blob, one URL; abort+revoke on replacement or
unmount) while still exposing only src to the existing render-prop
consumers.

AnnotatorCanvas exposes an optional onImageReady callback carrying a
generation-aware decoded-image lease over the real rendered
annotator-image element. The lease's readRgb performs lazy,
descriptor-frame RGBA-to-RGB conversion and refuses once its
generation or src is stale, so a retained source can never read a
replacement asset through a reused DOM node.

No annotator/browser-inference dependency, model change, or exposed
inference target is introduced; the headless annotator-core boundary
is untouched.
`docs/content/architecture/frontend/ui-core.md`'s durable rule now states
laziness, no-second-Image, and no-exposed-inference-capability explicitly, and
draws the line on what is proved versus claimed: no universal browser/Pillow
byte parity, only what a real browser measurably does.

`e2e/assetPixels.spec.ts` and its test-only harness
(`app/src/demo/AssetPixelsFixture.tsx`, routed at `/asset-pixels-fixture`,
ungated like `/demo` and `/styleguide`) are the real-browser proof: a tiny RGBA
image encoded to a PNG at runtime, no binary fixture, showing one content
request, the same decoded `<img>` reused as the pixel source, and the exact
descriptor-frame RGB a real 2D context produced.

`decodedAssetImage.test.ts`'s existing pixel-math test is strengthened to
assert the exact image reference and exact `getImageData` args, closing the
one gap jsdom's canvas leaves for a unit test to catch on its own.
tsconfig.e2e.json's project does not include src/demo, so the ambient
Window augmentation declared in AssetPixelsFixture.tsx was invisible to
e2e/assetPixels.spec.ts's own typecheck, failing frontend lint. Use the
same window-as-unknown-cast idiom _bench.ts already uses for this exact
cross-project situation instead of relying on a global declared outside
the e2e program.
…ease on unmount

The package's real public barrel (src/index.ts) re-exported the React
adapter through an explicit named list that omitted the two lease types,
so `import type { DecodedAssetImage } from "@visionset/annotator"` never
compiled even though the adapter module itself exported them. Add both
to the barrel's existing named list.

A real host switches assets by unmounting the old AnnotatorCanvas and
mounting a fresh one, not by changing imageSrc on a live instance, so the
generation bump keyed on imageSrc never fires on that path. A lease
handed out just before teardown kept its captured generation equal to
the (unmounted) instance's counter and its detached <img> still carried
the old src, so readRgb's guard could pass and hand back the previous
asset's pixels. Bump the generation ref in an unmount-only effect
cleanup so every outstanding lease from that instance genuinely refuses
afterward, through the same guard readRgb already checks.

Covering tests in decodedImageLease.test.tsx: an unmount() + readRgb
refusal test for the invalidation path, and a compile-time check (typed
via the real @visionset/annotator import, not a relative path) proving
DecodedAssetImage/RgbPixels actually surface publicly. Verified by
temporarily reverting the export and confirming ui-core's typecheck
fails with TS2459 before restoring it.

Updates the architecture doc's lease-refusal sentence to cover both
the imageSrc-replacement and the unmount path.
The Chromium proof for the browser pixel-reuse seam had its own standalone
route, asset-pixels-fixture, which is a test-only production surface with no
product link. Move it behind the existing /demo route's scene query
parameter, the same pattern the annotator benchmark already uses, so no
dedicated route exists purely to host a Playwright harness.
…ImageReady

The final review for this seam found that React always invokes the most
recently committed onLoad handler, never the one attached when a particular
load event was queued, so the existing generation/src guard cannot by itself
refuse a delayed load delivered after imageSrc changes twice on a live,
non-remounted instance. No host exercises that path today: every real asset
switch unmounts this component, which the separate unmount effect already
covers, and browsers do not dispatch load/error for an abandoned image
request in the first place. Record the residual in the two places a future
consumer would read them, rather than carry an unverifiable code change for
a race nothing here can exercise or test.
@JArmandoAnaya
JArmandoAnaya merged commit e7b5771 into main Sep 17, 2026
32 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the feat/browser-inference-asset-reuse branch September 17, 2026 01:26
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