Skip to content

barcodes: readability, review fixes, and performance - #317

Merged
shreeve merged 15 commits into
mainfrom
barcode-cleanups
Sep 16, 2026
Merged

shreeve merged 15 commits into
mainfrom
barcode-cleanups

Conversation

@shreeve

@shreeve shreeve commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Sixteen commits on packages/barcodes, each gated by the package tests, the encoder oracle (1452 checks byte-identical against paulmillr/qr), the decoder oracle (192 checks identical), and the benchmark.

Readability

  • Names that were wrong in context corrected; named constants for limits and finder flag bits; section headers; comment fixes; whitespace alignment (compiled output byte-identical before and after that pass).
  • Every raised error goes through fail msg, kind = Error; argument names in messages are unquoted.
  • Test expectations for ascii, term, raw, svg and gif are pictures or structural checks read back through small helpers instead of byte dumps.
  • Package header in the house style.

Review fixes

  • Decoder: images under five threshold blocks read past the grid and got a zero threshold; the fine plane clipped its height from the allocation; an unconstructible ECI label escaped as a RangeError; a prototype-inherited format name passed validation; a null image threw an engine message.
  • Browser layer: a VideoFrame could overwrite the arena of an in-flight async decode; overlay and result callbacks allocated per frame without a canvas; BarcodeDetector decoded stills on the camera budget; setStream leaked replaced tracks.
  • README: the camera example awaits readFrame; the VideoFrame path needs fullSize; the Code 128 subset example was false.

Performance

Workload Before After paulmillr/qr
encode raw v1 4.1 µs 2.4 µs 3.2 µs
encode raw v8 16.5 µs 14.7 µs 24.7 µs
encode raw v18 50.3 µs 46.4 µs 61.8 µs
encode svg v8 42.8 µs 31.0 µs 47.4 µs
encode gif v8 17.6 µs 14.6 µs 18.3 µs
decode raster v1 39.0 µs 28.5 µs 114.1 µs
decode 720p 643 µs 578 µs 1010 µs
decode 1080p 1470 µs 1300 µs 2210 µs
decode 1080p miss 24.2 ms 6.0 ms 23.9 ms
Code 128 read, 1080p hit 660 µs 8 µs

Best of three processes, Bun 1.4.0, Apple M5. Each change carries its own identity check in the commit message (corrupted-symbol sweep, per-layer threshold arrays, exhaustive mask table, 261 Code 128 images across nine pixel formats). Int32Array words throughout, so a full word never deoptimizes the JIT to doubles.

Full suite: 26 lanes, 9896 tests.

Encoder and decoder output is unchanged: the encoder oracle (1452 checks)
and decoder oracle (192 checks) are identical against the reference, the
package tests pass 33/33, and every benchmark row is equal or faster.

qr.rip
- Decoder bitmap and the resize word view are Int32Array, matching the
  encoder matrix: every consumer is a bit operation, and a signed view
  keeps JavaScriptCore on its int32 path instead of deoptimizing to
  doubles once a word has bit 31 set. `>>> 0` coercions dropped where
  the store no longer needs them.
- Names that were wrong in context: `words` (ECC codeword count) is
  `eccWords`, since bare `words` means matrix words; `blockLen` (data
  length) is `shortData`/`shortBlock`; the three run caps share `cap`.
- Named constants for the size and search limits and the finder flag
  bits, with a schema comment for the flag word.
- `asVersion` delegates its type and integer checks to `asNum`;
  `lengthClass`, `sizeOf`, `versionOf` and a `fits` closure replace
  repeated expressions; `EXP`/`LOG` hoisted after GF256.
- `_tests` holds only what the tests use.
- Section headers, comment corrections, long lines broken.

code128.rip
- Shares `fail`, `asString` and `asObject` with qr.rip through image.rip
  instead of private copies; `checksum` shared by encoder and reader;
  the dead `subset` parameter of `charValue` dropped.
- `encodeCode128` validates `output` as a string and `opts` as a
  non-array object, and its validator titles name the option as
  `opts.scale`, `opts.border`, `opts.height`, matching qr.rip.
- Section headers and a legend for the subset transition table.

image.rip
- Validation section exports `fail`, `asString`, `asObject`.
- `copyLuma` block-copies a whole plane when the stride is the width,
  and copies row by row otherwise; the word view is Int32Array.

gif.rip
- Signature, chunk and header sizes are named constants with the
  header derivation in a comment.

dom.rip
- `setCanvasSize(canvas, width, height)` is width-first like every
  other size call; all six callers updated. Public `drawImage` keeps
  its order.
- Section headers, `=!` on helpers, constructor field-group comments.

test.rip
- Expectations for ascii, term, raw, svg and gif are pictures or
  structural checks read back through small helpers instead of
  byte dumps; exact bytes stay pinned by the oracles.

README.md
- Runtime paragraph corrected; Int32Array named consistently.

Whitespace alignment applied across all files; compiled output is
byte-identical before and after that pass.
`fail msg, kind = Error` throws `kind.new msg`. The 43 sites that spelled
`throw Error.new`, `throw TypeError.new` or `throw RangeError.new` now
call it, with the class as a trailing argument where it is not Error.
Messages and classes are unchanged: the encoder oracle compares both
against the reference (1452 checks, identical), the decoder oracle is
identical (192 checks), tests pass 33/33, benchmarks equal or faster.

Left as throws on purpose: rethrows of a caught error, the BarcodeDetector
polyfill's DOMException and browser-worded TypeError messages, promise
rejections, and the frozen FAIL sentinels, which are values, not raises.
'text expected string, got type=number' instead of the same message with
the name wrapped in escaped double quotes. Thirteen messages across
qr.rip, image.rip and code128.rip; the size validators already read this
way. The one message that still quotes is the unknown-letter error, where
the quotes delimit a character that may be a space.

Tests 33/33; the encoder oracle compares messages with the reference's
quotes stripped and is identical (1452 checks).
Decoder
- A format name inherited from Object.prototype ('toString') no longer
  passes validation: FORMATS has a null prototype.
- A missing image fails with the package's own message instead of an
  engine TypeError.
- Images under five threshold blocks (a side below 40 px) fold the 5x5
  window onto their edge blocks instead of reading past the grid, which
  zeroed the threshold and lost every module lighter than luma 0.
- The fine plane carries its own block-row count; the upgrade path
  clipped its height from the allocation instead of the live grid and
  could sample a stale threshold row at the bottom.
- An ECI whose label the runtime cannot construct returns FAIL.data
  instead of escaping the frame path as a RangeError.

Browser layer
- Overlay and result callbacks are installed only when their canvas
  exists, so a scanner without them allocates nothing per frame.
- The VideoFrame path declines a new frame while an async decode still
  reads the arena.
- BarcodeDetector decodes stills with unlimited effort and time.
- setStream stops the stream it replaces; the source reset is one
  helper; svgToPng accepts a 8192 px side; the unreachable RGB plane
  entry is gone.

Encoder: gs1 is truthy like optimize; the unused penalty wrapper, an
unreachable text-decoder guard, a dead store and the write-only MEMBER
flag are removed; evalLow sits with the GF(256) helpers; retry sets and
decode drivers have their own section headers.

README: the camera example awaits readFrame; the VideoFrame path needs
fullSize; the Code 128 subset example is '123'; svgToPng and gifToPng
result types are stated.

Tests: 34 (small low-contrast decode, prototype format name, null
image); the checksum check is an inline mod-103 sum; svgCells derives
its envelope from the width and checks the return-edge command; section
banners use p.

Encoder oracle 1452/0, decoder oracle 192/0, every benchmark row equal
or faster than the pre-cleanup baseline.
findRows measured each pixel run through run(), which re-reads the layer
fields, bounds-checks, and clamps against an infinite cap on every call:
about 550k calls on a 1080p frame with no symbol, 80% of decode time.
The row walker now consumes words directly with the same clz32 logic,
holding width, words and bitmap in locals. run() is unchanged for the
capped, vertical callers.

Finder patterns identical (decoder oracle 192/0), encoder oracle 1452/0,
tests 34/34. Decode 1080p miss 20.0 ms to 7.2 ms; raster v1 37.1 to
33.3 µs; 720p and 1080p equal or faster; encode rows unchanged.
readCode128 converted the whole frame to a luma plane and then read
about 32 lines of it; on a 1080p hit the conversion was 627 µs of a
656 µs read. Each scan line is now sampled from the source pixels as it
is reached, with copyLuma's formulas: (r + 2g + b) / 4 for packed RGB in
any byte order, the byte for 8-bit planes, the top eight bits for 10 and
12-bit planes. One line buffer replaces the frame-sized plane.

Identical results on 261 images across nine pixel formats (189 hits, 72
misses); tests 34/34; QR rows unchanged. Read 720p hit 294 to 5 µs,
1080p blank 711 to 106 µs, 1080p noise 1123 to 474 µs.
correctBlock evaluated every syndrome through mul() before learning a
block was clean; at version 40 that was three quarters of decodeGrid.
The block's LFSR remainder, computed with the encoder's own products
table into the free next buffer, is zero exactly when every syndrome is
zero, so a zero remainder returns before the syndrome loop. Corrupted
blocks take the unchanged path.

Identical results on 896 corrupted-symbol cases (v1..40, four ECC
levels, 0..160 flipped modules); decoder oracle 192/0; encoder oracle
1452/0; tests 34/34. Raster v1 33.3 to 32.5 µs, 1080p 1.43 to 1.41 ms,
decodeGrid at size 177 roughly halved; encode rows unchanged.
finderPenaltyVertical reloaded eleven matrix words at every row step.
Each word column now loads ten words once and one new word per step,
shifting the registers down; the masks and counts are unchanged.

Encoder oracle byte-identical (1452/0), decoder oracle 192/0, tests
34/34. Encode raw v1 2.7 to 2.4 µs, v8 16.5 to 14.3, v18 49.8 to 45.3;
svg v8 41.3 to 40.1; gif v8 17.7 to 15.7; decode rows unchanged.
renderSvg materialized both the absolute and the relative move for every
dark module to compare their lengths. The lengths are now counted from
the digits, only the shorter command is built, and the cell is appended
as one template with the return-edge command chosen the same way.

Byte-identical svg on 623 cases (versions 1..40, four ECC levels,
borders 1..4, scales 1..3, unoptimized, and four-digit coordinates at the
1024 px output cap); encoder oracle 1452/0; tests 34/34. Encode svg v8
40.1 to 31.1 µs; every other row unchanged.
blockRows and bitmapRows re-read the layer fields inside their loops and
ran a fixed eight-iteration inner loop per block row that the JIT left
as a loop. The layer fields are locals, the eight loads, compares and
the packed byte are written out, and the 5x5 threshold window is one
five-term add per row on the full-grid branch. The clamped small-grid
branch and the bitmap store are unchanged; BLOCK is fixed at 8 in this
section already.

Per-layer blocks, cuts and bitmap identical on 18 images and 51 layers;
decoder oracle 192/0; encoder oracle 1452/0; tests 34/34. Decode raster
v1 32.7 to 29.1 µs, 720p 629 to 571 µs, 1080p 1.42 to 1.31 ms, 1080p
miss 7.3 to 5.9 ms; encode rows unchanged.
Every Table 10 mask predicate repeats every 6 columns and 12 rows, so
the 8-bit predicate vector for a module is a lookup. maskCalc keeps the
arithmetic and fills the table once at load; maskBits reads it. The
decoder's unmask loop and the encoder's per-version cache no longer
evaluate six modulos per module.

Table equals the arithmetic at all 32761 points of 0..180 squared;
encoder oracle 1452/0; decoder oracle 192/0; tests 34/34. Decode raster
v1 30.0 to 28.5 µs against a same-session baseline; other rows
unchanged.
writeGif made a subarray view for every span of at most 126 pixels; at
the 4096 px cap that is over 130k views per image. A byte loop over the
span replaces the view.

Encoder oracle byte-identical on gif and data-url (1452/0); tests 34/34.
Encode gif v8 15.9 to 14.6 µs; other rows unchanged.
Best of three processes, Bun 1.4.0, Apple M5, same sequence as before;
the miss-case sentence names the packed-word finder walk.
One column per table: the reference time divided by this package's,
from the figures already in the row.
Author, date, and an Engine paragraph naming the packed-word symbol
representation, the decode pipeline, the formats, and the origin.
@shreeve
shreeve merged commit 2baf165 into main Sep 16, 2026
2 checks passed
@shreeve
shreeve deleted the barcode-cleanups branch September 16, 2026 18:17
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