Skip to content

EPIC 8: frame text arena + slice-referenced text ops#218

Merged
RtlZeroMemory merged 4 commits intomainfrom
epic8-zero-copy-text-arena
Feb 26, 2026
Merged

EPIC 8: frame text arena + slice-referenced text ops#218
RtlZeroMemory merged 4 commits intomainfrom
epic8-zero-copy-text-arena

Conversation

@RtlZeroMemory
Copy link
Owner

@RtlZeroMemory RtlZeroMemory commented Feb 26, 2026

Summary

  • Add FrameTextArena (power-of-two growth, encodeInto, allocUtf8/allocBytes, reserve, counters)
  • Route transient drawText/addTextRunBlob text through arena slices {stringIndex=0, byteOff, byteLen}
  • Keep persistent link strings in string spans after arena bytes
  • Add reserveTextArena + getTextPerfCounters APIs
  • Fix link ref indexing bug in setLink
  • Update drawlist layout writing/validation for single arena span + shifted persistent spans
  • Update drawlist tests and regenerate golden fixtures for new wire bytes
  • Add new tests: builder.text-arena.equivalence.test.ts (text-heavy framebuffer equivalence, 50k segment stress bounds, randomized property slices)
  • Add benchmark script packages/core/scripts/bench-text-arena.mjs and package script bench:text-arena

Verification

  • npm run build (repo root): pass
  • packages/core drawlist tests: 417/417 pass

Benchmark

  • segments=20000, iterations=40
  • legacy avg: 7.337ms
  • arena avg: 1.875ms
  • estimated allocations: 20001 -> 1
  • speedup: 3.91x

Summary by CodeRabbit

  • New Features

    • Added a per-frame text arena, new drawlist text performance counters, and builder ops: setLink, clear, reserveTextArena, getTextPerfCounters.
  • Performance Improvements

    • Text encoding now uses arena-backed allocation, reducing allocations and exposing encoder/arena metrics.
  • Chores

    • Added a benchmark script to run text-arena performance tests.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between 1f3f962 and 6df1bef.

📒 Files selected for processing (7)
  • packages/core/src/__tests__/integration/integration.dashboard.test.ts
  • packages/core/src/__tests__/integration/integration.file-manager.test.ts
  • packages/core/src/__tests__/integration/integration.form-editor.test.ts
  • packages/core/src/__tests__/integration/integration.reflow.test.ts
  • packages/core/src/__tests__/integration/integration.resize.test.ts
  • packages/core/src/drawlist/__tests__/builder.align4.test.ts
  • packages/core/src/drawlist/builderBase.ts
 ________________________________________________
< I raised Series-B funding to review your code. >
 ------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

✏️ Tip: You can disable in-progress messages and the fortune message in your review settings.

📝 Walkthrough

Walkthrough

Adds a per-frame UTF‑8 text arena and integrates arena-backed text allocation/encoding into the drawlist builder (new FrameTextArena, text-perf counters, reserve/get APIs, builder signature changes, layout changes), updates many tests to arena semantics, and adds a benchmark script comparing legacy vs arena encoding.

Changes

Cohort / File(s) Summary
Benchmark & Scripts
packages/core/package.json, packages/core/scripts/bench-text-arena.mjs
Added bench:text-arena npm script and a Node benchmark that compares legacy TextEncoder encoding vs FrameTextArena encoding.
Text Arena Implementation
packages/core/src/drawlist/textArena.ts
New FrameTextArena class and related types (TextArenaSlice, TextArenaCounters, Utf8EncoderInto) managing per‑frame UTF‑8 allocations, growth, and counters.
Builder Core & Integration
packages/core/src/drawlist/builderBase.ts, packages/core/src/drawlist/builder.ts
Integrated text arena into builder flow: allocTextSlice, reserveTextArena, getTextPerfCounters; build/write layout updated to include arena bytes; append/write signatures updated to accept byteOff; setLink reference encoding adjusted (removed +1).
Public Types & Exports
packages/core/src/drawlist/types.ts, packages/core/src/drawlist/index.ts, packages/core/src/index.ts
New exported type DrawlistTextPerfCounters; optional reserveTextArena and getTextPerfCounters on DrawlistBuilder; re-export of DrawlistTextPerfCounters.
Encoding & Command Layouts
packages/core/src/drawlist/__tests__/*, packages/core/src/drawlist/builder.ts, packages/core/src/drawlist/builderBase.ts
Numerous test updates and binary/golden adjustments to reflect arena-based string storage, shifted offsets/sizes, added byteOff fields, changed per-segment blob metadata and command payload sizes.
Tests: Interning, Cache & Equivalence
packages/core/src/drawlist/__tests__/builder.string-intern.test.ts, packages/core/src/drawlist/__tests__/builder.string-cache.test.ts, packages/core/src/drawlist/__tests__/builder.text-arena.equivalence.test.ts
Refactored tests to validate arena slices (byteOff/byteLen), updated caching semantics and counters, and added an extensive equivalence/stress test suite for arena behavior.
API/Behavioral Test Updates
packages/core/src/drawlist/__tests__/builder.limits.test.ts, packages/core/src/drawlist/__tests__/builder.reset.test.ts, packages/core/src/drawlist/__tests__/builder.validate-caps.test.ts
Tests changed to use setLink for persistent strings, replace maxStrings with maxDrawlistBytes, add clear(), and validate persistent vs transient string behavior.
Small Adjustments
packages/core/src/drawlist/__tests__/builder.alignment.test.ts, packages/core/src/drawlist/__tests__/builder.graphics.test.ts, packages/core/src/drawlist/__tests__/builder.text-run.test.ts, packages/core/src/drawlist/__tests__/builder_style_attrs.test.ts
Minor test updates: consolidated arena assertions, header version/field adjustments, hyperlink/index decoding changes, underline color encoding, and changed per-segment attribute stride.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Builder as DrawlistBuilder
    participant Arena as FrameTextArena
    participant Encoder as Utf8Encoder
    participant Buffer as BinaryBuffer

    Client->>Builder: drawText(text, style)
    Builder->>Builder: allocTextSlice(text)
    Builder->>Arena: allocUtf8(text)
    Arena->>Arena: reserve if needed
    Arena->>Encoder: encodeInto(text)
    Encoder-->>Arena: encoded bytes
    Arena->>Arena: update counters (textEncoderCalls, textSegments, bytes)
    Arena-->>Builder: TextArenaSlice {off, len}
    Builder->>Builder: appendDrawTextCommand(..., byteOff, byteLen)
    Builder->>Buffer: write header, commands, and arena bytes
    Buffer-->>Client: encoded drawlist
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and stash them neat,
UTF‑8 slices make my burrow sweet.
Encoders whisper, arenas grow wide,
Commands aligned, offsets neatly tied.
Hop, benchmark—our text now takes the ride!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a frame text arena system with slice-referenced text operations, which aligns with the substantial refactoring of text handling throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch epic8-zero-copy-text-arena

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/drawlist/__tests__/builder.alignment.test.ts (1)

267-274: ⚠️ Potential issue | 🟡 Minor

Formatting needs to be applied in this updated assertion block.

CI reports formatter-required changes in this range; please run the formatter so the test file passes pipeline checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/drawlist/__tests__/builder.alignment.test.ts` around lines
267 - 274, The updated assertion block around the test "reuseOutputBuffer keeps
odd-string padding zeroed across reset/build cycles" is not properly formatted;
run the project's code formatter (e.g., npm run format or prettier) and reformat
the assertions that reference h, bytes, h.stringsBytesLen, h.stringsBytesOffset
and the String.fromCharCode(...) check so the file
packages/core/src/drawlist/__tests__/builder.alignment.test.ts adheres to the
repository formatting rules and the CI will pass.
🧹 Nitpick comments (1)
packages/core/package.json (1)

19-19: Build before benchmarking to avoid stale/missing dist artifacts.

bench-text-arena.mjs imports from ../dist/... (Line 3 in the script), so running this on a clean checkout (or after source edits) can fail or benchmark stale output.

♻️ Proposed script tweak
-    "bench:text-arena": "node scripts/bench-text-arena.mjs"
+    "bench:text-arena": "npm run build && node scripts/bench-text-arena.mjs"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/package.json` at line 19, The bench script ("bench:text-arena")
runs node scripts/bench-text-arena.mjs which imports from ../dist, causing
failures or stale benchmarks on clean checkouts; update the "bench:text-arena"
npm script to first build the package (run the existing build script, e.g., "npm
run build" or the monorepo build command) before invoking node
scripts/bench-text-arena.mjs so that ../dist is fresh and present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/scripts/bench-text-arena.mjs`:
- Around line 84-86: The three-line block with the bench calls (symbols: bench,
legacy, arena, legacyEncodeFrame, arenaEncodeFrame) is misformatted and failing
CI; run the repository's code formatter (e.g., prettier/format script) on
packages/core/scripts/bench-text-arena.mjs (or run the project’s lint/format npm
script) to reformat those lines so they match project style, then commit the
resulting changes so CI passes.
- Around line 5-7: The benchmark reads SEGMENTS, ITERATIONS, and WARMUP from env
but doesn't validate them, causing avgMs (and other metrics) to divide by zero
or produce NaN/Infinity; update the parsing/initialization for SEGMENTS,
ITERATIONS, and WARMUP to ensure they are finite positive integers (fallback to
the existing defaults if Number(...) yields NaN or non-positive/Infinity), and
add a runtime guard before computing avgMs (and any other division by
iterations) to ensure iterations > 0 — if invalid, either clamp to 1 or emit a
clear error and exit; reference the constants SEGMENTS, ITERATIONS, WARMUP and
the avgMs calculation to locate where to add validation and the guard.

In `@packages/core/src/drawlist/__tests__/builder.text-arena.equivalence.test.ts`:
- Around line 90-108: The failing CI formatter error is due to formatting in the
decodeTextSlice function; open the decodeTextSlice implementation (references:
function decodeTextSlice, Header type h, helper u32, and TextDecoder usage) and
reformat the block to match project style (or run the project's
formatter/Prettier/ESLint --fix) so spacing/line breaks around the return and
asserts conform to the codebase rules, then commit the formatted file.

In `@packages/core/src/drawlist/builderBase.ts`:
- Around line 1054-1061: The CI formatter flagged formatting around the
calculations for string/span offsets in builderBase.ts — reformat the block
defining persistentStringsCount, hasTextArenaSpan, stringsCount,
stringsSpanBytes, stringsSpanOffset, stringsBytesOffset, and stringsBytesAligned
to match the project's code style (run the repository formatter or adjust
whitespace/line breaks to align with surrounding code), preserving the exact
logic and variable names (persistentStringsCount, hasTextArenaSpan,
stringsCount, stringsSpanBytes, stringsSpanOffset, stringsBytesOffset,
stringsBytesAligned).
- Around line 157-160: Reformat the TextEncoder initialization block to match
the repo's formatter style: adjust spacing and line breaks around the typed
globalThis cast and the ternary so the declaration of textEncoderCtor and
assignment to this.encoder follow the project's formatting (e.g., collapse into
fewer lines or align parentheses as other files do); ensure the typed cast to
Readonly<{ TextEncoder?: new () => Utf8Encoder }> remains, and keep the
identifiers textEncoderCtor, Utf8Encoder and this.encoder unchanged. Run the
project's automatic formatter (e.g., Prettier/format task) after editing to
confirm CI will pass.

In `@packages/core/src/drawlist/textArena.ts`:
- Around line 38-47: The constructor parameter order in TextArena is wrong for
the linter: move the required encoder parameter (Utf8EncoderInto) before the
optional initialCapacity with its default (DEFAULT_INITIAL_CAPACITY) in the
TextArena constructor signature, update the internal variable names/uses
(initialCapacity, encoder, buf) accordingly, and then update all call sites that
construct TextArena to pass the encoder first and the optional initialCapacity
second so the capacity selection logic (Number.isFinite/Number.isInteger checks
and Uint8Array allocation) remains unchanged.

---

Outside diff comments:
In `@packages/core/src/drawlist/__tests__/builder.alignment.test.ts`:
- Around line 267-274: The updated assertion block around the test
"reuseOutputBuffer keeps odd-string padding zeroed across reset/build cycles" is
not properly formatted; run the project's code formatter (e.g., npm run format
or prettier) and reformat the assertions that reference h, bytes,
h.stringsBytesLen, h.stringsBytesOffset and the String.fromCharCode(...) check
so the file packages/core/src/drawlist/__tests__/builder.alignment.test.ts
adheres to the repository formatting rules and the CI will pass.

---

Nitpick comments:
In `@packages/core/package.json`:
- Line 19: The bench script ("bench:text-arena") runs node
scripts/bench-text-arena.mjs which imports from ../dist, causing failures or
stale benchmarks on clean checkouts; update the "bench:text-arena" npm script to
first build the package (run the existing build script, e.g., "npm run build" or
the monorepo build command) before invoking node scripts/bench-text-arena.mjs so
that ../dist is fresh and present.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a55167 and 0931e4a.

⛔ Files ignored due to path filters (3)
  • packages/testkit/fixtures/zrdl-v1/golden/clip_nested.bin is excluded by !**/*.bin
  • packages/testkit/fixtures/zrdl-v1/golden/draw_text_interned.bin is excluded by !**/*.bin
  • packages/testkit/fixtures/zrdl-v1/golden/fill_rect.bin is excluded by !**/*.bin
📒 Files selected for processing (20)
  • packages/core/package.json
  • packages/core/scripts/bench-text-arena.mjs
  • packages/core/src/drawlist/__tests__/builder.alignment.test.ts
  • packages/core/src/drawlist/__tests__/builder.golden.test.ts
  • packages/core/src/drawlist/__tests__/builder.graphics.test.ts
  • packages/core/src/drawlist/__tests__/builder.limits.test.ts
  • packages/core/src/drawlist/__tests__/builder.reset.test.ts
  • packages/core/src/drawlist/__tests__/builder.round-trip.test.ts
  • packages/core/src/drawlist/__tests__/builder.string-cache.test.ts
  • packages/core/src/drawlist/__tests__/builder.string-intern.test.ts
  • packages/core/src/drawlist/__tests__/builder.text-arena.equivalence.test.ts
  • packages/core/src/drawlist/__tests__/builder.text-run.test.ts
  • packages/core/src/drawlist/__tests__/builder.validate-caps.test.ts
  • packages/core/src/drawlist/__tests__/builder_style_attrs.test.ts
  • packages/core/src/drawlist/builder.ts
  • packages/core/src/drawlist/builderBase.ts
  • packages/core/src/drawlist/index.ts
  • packages/core/src/drawlist/textArena.ts
  • packages/core/src/drawlist/types.ts
  • packages/core/src/index.ts

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0931e4afd7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@RtlZeroMemory
Copy link
Owner Author

Addressed all outstanding review comments and pushed follow-ups:\n\n- : formatter/lint feedback + benchmark script hardening\n - bench script now builds first (avoids stale/missing )\n - env inputs are validated as positive integers\n - added runtime guard for invalid iteration counts\n - constructor param order fixed for \ and call sites updated\n - formatter fixes applied to flagged files\n\n- : safety preflights for large text allocations\n - \ now bounds-checks estimated total size before reserving\n - transient text allocation preflights UTF-8 length + estimated frame size before encoding\n - added validation tests for both failure paths\n\nVerification:\n- \ (packages/core) ✅\n- drawlist tests: \ passed ✅\n- \ ✅\n\nAlso resolved the two open Codex review threads on this PR.

@RtlZeroMemory
Copy link
Owner Author

Addressed all outstanding review comments and pushed follow-ups:

  • 6c38c4a: formatter/lint feedback + benchmark script hardening

    • bench:text-arena now runs build first (avoids stale/missing dist artifacts)
    • benchmark env inputs are validated as positive integers
    • runtime guard added for invalid iteration counts
    • FrameTextArena constructor param order fixed (required before defaulted) and call sites updated
    • formatter fixes applied to all flagged files
  • 1f3f962: safety preflights for large text allocations

    • reserveTextArena() now bounds-checks estimated total frame size before reserve
    • transient text allocation now preflights UTF-8 length + estimated frame size before arena encode
    • added tests for both failure paths (builder.validate-caps.test.ts)

Verification:

  • npm run build in packages/core
  • drawlist tests: 419/419 passed ✅
  • npm run bench:text-arena

Also resolved the two previously open Codex review threads on this PR.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/drawlist/builderBase.ts (1)

466-490: ⚠️ Potential issue | 🟠 Major

Require full test suite run evidence before merge.

Binary protocol builder changes are a danger zone. While drawlist tests provide unit coverage, the full test suite (node scripts/run-tests.mjs) must run before merge to catch subtle regressions across rendering, layout, and runtime integration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/drawlist/builderBase.ts` around lines 466 - 490, The PR
modifies binary protocol builder logic (see buildWithVersion and
buildIntoWithVersion in builderBase.ts) which can cause subtle regressions;
before merging, run the full test suite via the project's canonical runner (node
scripts/run-tests.mjs) and include the full test output as proof in the PR,
addressing any failing tests (especially around rendering, layout, and runtime
integration) and not just the drawlist unit tests; attach the test log and any
fixes to failing targets before approving the merge.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/drawlist/builderBase.ts`:
- Around line 748-754: allocTextSlice and reserveTextArena bypass the
maxStringBytes limit causing textArena.byteLength() + newStringBytes to exceed
the allowed budget; before allocating or reserving, compute the UTF-8 byte
length of the incoming text (using the existing encoder) and check that
textArena.byteLength() + stringBytesLen <= maxStringBytes; if the check fails
call this.fail("ZRDL_INTERNAL", "...") and return null (or false) to prevent
allocation. Apply the same guard to reserveTextArena and any other arena-backed
text allocation paths referenced (e.g., the other alloc/reserve sites and
internString interactions) so all arena allocations respect maxStringBytes.

---

Outside diff comments:
In `@packages/core/src/drawlist/builderBase.ts`:
- Around line 466-490: The PR modifies binary protocol builder logic (see
buildWithVersion and buildIntoWithVersion in builderBase.ts) which can cause
subtle regressions; before merging, run the full test suite via the project's
canonical runner (node scripts/run-tests.mjs) and include the full test output
as proof in the PR, addressing any failing tests (especially around rendering,
layout, and runtime integration) and not just the drawlist unit tests; attach
the test log and any fixes to failing targets before approving the merge.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0931e4a and 6c38c4a.

📒 Files selected for processing (6)
  • packages/core/package.json
  • packages/core/scripts/bench-text-arena.mjs
  • packages/core/src/drawlist/__tests__/builder.alignment.test.ts
  • packages/core/src/drawlist/__tests__/builder.text-arena.equivalence.test.ts
  • packages/core/src/drawlist/builderBase.ts
  • packages/core/src/drawlist/textArena.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/core/src/drawlist/tests/builder.alignment.test.ts
  • packages/core/src/drawlist/textArena.ts
  • packages/core/package.json
  • packages/core/scripts/bench-text-arena.mjs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/core/src/drawlist/builderBase.ts (1)

207-230: ⚠️ Potential issue | 🟠 Major

maxStringBytes is still bypassed for arena-backed text.

reserveTextArena()/allocTextSlice() enforce maxDrawlistBytes, but layout/build combine arena bytes with persistent string bytes into one strings payload. This still allows maxStringBytes to be exceeded without tripping a cap.

💡 Suggested guard (combined string budget)
@@
   reserveTextArena(bytes: number): void {
@@
     const nextRequired = this.textArena.byteLength() + bi;
+    const projectedStringBytes = nextRequired + this.stringBytesLen;
+    if (projectedStringBytes > this.maxStringBytes) {
+      this.fail(
+        "ZRDL_TOO_LARGE",
+        `reserveTextArena: maxStringBytes exceeded (bytes=${projectedStringBytes}, max=${this.maxStringBytes})`,
+      );
+      return;
+    }
@@
   protected allocTextSlice(text: string): TextArenaSlice | null {
@@
     const nextArenaLen = this.textArena.byteLength() + utf8Len;
+    const projectedStringBytes = nextArenaLen + this.stringBytesLen;
+    if (projectedStringBytes > this.maxStringBytes) {
+      this.fail(
+        "ZRDL_TOO_LARGE",
+        `drawText: maxStringBytes exceeded (bytes=${projectedStringBytes}, max=${this.maxStringBytes})`,
+      );
+      return null;
+    }
@@
     const stringsBytesRawLen = textArenaBytesLen + persistentStringsBytesLen;
+    if (stringsBytesRawLen > this.maxStringBytes) {
+      return {
+        ok: false,
+        error: {
+          code: "ZRDL_TOO_LARGE",
+          detail: `build: maxStringBytes exceeded (bytes=${stringsBytesRawLen}, max=${this.maxStringBytes})`,
+        },
+      };
+    }

Also applies to: 548-560, 760-789

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/drawlist/builderBase.ts` around lines 207 - 230,
reserveTextArena and allocTextSlice only check maxDrawlistBytes, allowing
combined arena-backed text + persistent string bytes to exceed maxStringBytes;
update reserveTextArena (and the same checks in allocTextSlice and the other
noted locations) to compute a combined string budget using
this.textArena.byteLength() + arenaAllocation + persistentStringsTotal (use the
same persistent string accounting used elsewhere) and fail with the appropriate
error when that combined total would exceed maxStringBytes rather than only
checking maxDrawlistBytes; reference validateParams/requireI32NonNeg,
estimateTotalSizeWithArenaState, textArena.reserve, maybeFailTooLargeAfterWrite
and ensure the new guard mirrors the failure behavior used for maxStringBytes
elsewhere.
🧹 Nitpick comments (1)
packages/core/src/drawlist/builderBase.ts (1)

245-251: allocTextSlice() emits drawText-specific error details for multiple callers.

This helper is used by both drawText and addTextRunBlob, but failures always report drawText: ..., which makes diagnostics misleading.

🧹 Suggested refactor for caller-specific errors
-    const textSlice = this.allocTextSlice(text);
+    const textSlice = this.allocTextSlice("drawText", text);
@@
-      const textSlice = this.allocTextSlice(seg0.text);
+      const textSlice = this.allocTextSlice("addTextRunBlob", seg0.text);
@@
-  protected allocTextSlice(text: string): TextArenaSlice | null {
+  protected allocTextSlice(method: "drawText" | "addTextRunBlob", text: string): TextArenaSlice | null {
@@
-      this.fail("ZRDL_INTERNAL", "drawText: TextEncoder is not available");
+      this.fail("ZRDL_INTERNAL", `${method}: TextEncoder is not available`);
@@
-        `drawText: maxDrawlistBytes exceeded (utf8Len=${utf8Len}, max=${this.maxDrawlistBytes})`,
+        `${method}: maxDrawlistBytes exceeded (utf8Len=${utf8Len}, max=${this.maxDrawlistBytes})`,
@@
-        `drawText: maxDrawlistBytes exceeded (estimatedTotal=${nextEstimatedTotal}, max=${this.maxDrawlistBytes})`,
+        `${method}: maxDrawlistBytes exceeded (estimatedTotal=${nextEstimatedTotal}, max=${this.maxDrawlistBytes})`,

Also applies to: 363-368, 760-784

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/drawlist/builderBase.ts` around lines 245 - 251,
allocTextSlice currently emits errors labeled for drawText even when called from
other callers like addTextRunBlob; update the callers (e.g., drawText and
addTextRunBlob where allocTextSlice is invoked and lines around
appendDrawTextCommand/encodeDrawTextStyle) to detect a null/this.error result
and, instead of relying on allocTextSlice to log, have each caller produce its
own specific error message (e.g., "drawText: ..." vs "addTextRunBlob: ...")
before returning; remove or demote the hardcoded drawText text in
allocTextSlice's error path so allocTextSlice only returns failure, and ensure
callers construct and log the appropriate, caller-specific diagnostic when they
see textSlice === null or this.error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/drawlist/builderBase.ts`:
- Around line 559-560: The current align4 implementation uses bitwise arithmetic
which can overflow due to JS coercion to signed 32-bit ints; update the align4
function (referenced where stringsBytesLen is computed and used) to a
modulo-based implementation that returns n if n % 4 === 0 else n + (4 - (n % 4))
so it works correctly for values near INT32_MAX; then ensure the call site
computing stringsBytesLen (the expression stringsCount === 0 ? 0 :
align4(stringsBytesRawLen)) remains unchanged so cursor arithmetic (cursor +=
stringsBytesLen) uses the safe alignment result.

---

Duplicate comments:
In `@packages/core/src/drawlist/builderBase.ts`:
- Around line 207-230: reserveTextArena and allocTextSlice only check
maxDrawlistBytes, allowing combined arena-backed text + persistent string bytes
to exceed maxStringBytes; update reserveTextArena (and the same checks in
allocTextSlice and the other noted locations) to compute a combined string
budget using this.textArena.byteLength() + arenaAllocation +
persistentStringsTotal (use the same persistent string accounting used
elsewhere) and fail with the appropriate error when that combined total would
exceed maxStringBytes rather than only checking maxDrawlistBytes; reference
validateParams/requireI32NonNeg, estimateTotalSizeWithArenaState,
textArena.reserve, maybeFailTooLargeAfterWrite and ensure the new guard mirrors
the failure behavior used for maxStringBytes elsewhere.

---

Nitpick comments:
In `@packages/core/src/drawlist/builderBase.ts`:
- Around line 245-251: allocTextSlice currently emits errors labeled for
drawText even when called from other callers like addTextRunBlob; update the
callers (e.g., drawText and addTextRunBlob where allocTextSlice is invoked and
lines around appendDrawTextCommand/encodeDrawTextStyle) to detect a
null/this.error result and, instead of relying on allocTextSlice to log, have
each caller produce its own specific error message (e.g., "drawText: ..." vs
"addTextRunBlob: ...") before returning; remove or demote the hardcoded drawText
text in allocTextSlice's error path so allocTextSlice only returns failure, and
ensure callers construct and log the appropriate, caller-specific diagnostic
when they see textSlice === null or this.error.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c38c4a and 1f3f962.

📒 Files selected for processing (2)
  • packages/core/src/drawlist/__tests__/builder.validate-caps.test.ts
  • packages/core/src/drawlist/builderBase.ts

@RtlZeroMemory
Copy link
Owner Author

Addressed the align4 overflow review comment and resolved the thread.

Changes in 6df1bef:

  • Replaced align4 bitwise implementation with modulo arithmetic to avoid signed 32-bit coercion overflow near INT32_MAX.
  • Added regression test packages/core/src/drawlist/__tests__/builder.align4.test.ts covering boundary values up to 2147483647.
  • Updated integration drawlist string parsers to decode arena-backed DRAW_TEXT and text-run blob slices, restoring text assertions under the text-arena protocol.

Validation:

  • npm run build (packages/core) ✅
  • node --test --test-concurrency=1 dist/drawlist/__tests__/builder.align4.test.js dist/drawlist/__tests__/builder.text-arena.equivalence.test.js dist/drawlist/__tests__/builder.validate-caps.test.js dist/__tests__/integration/integration.dashboard.test.js dist/__tests__/integration/integration.reflow.test.js dist/__tests__/integration/integration.resize.test.js (packages/core) ✅

@RtlZeroMemory RtlZeroMemory merged commit 9d94eb7 into main Feb 26, 2026
6 of 8 checks passed
@RtlZeroMemory RtlZeroMemory deleted the epic8-zero-copy-text-arena branch February 28, 2026 04:18
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