Skip to content

perf(start-client-core): O(1) buffer drain in client frame decoder#7663

Open
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/frame-decoder-index-pointer
Open

perf(start-client-core): O(1) buffer drain in client frame decoder#7663
anonrig wants to merge 1 commit into
TanStack:mainfrom
anonrig:perf/frame-decoder-index-pointer

Conversation

@anonrig

@anonrig anonrig commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

What

Replace the O(n) bufferList.shift() in the client-side frame decoder (packages/start-client-core/src/client-rpc/frame-decoder.ts) with an O(1) head pointer.

extractFlattened() dropped each fully-consumed chunk from the front of bufferList with shift(). When a single large frame (e.g. a big RawStream payload) is assembled from many small network reads, the extract loop calls shift() once per chunk — and each shift() re-indexes the whole array, so reassembly degrades to O(n²).

This PR tracks the first un-consumed chunk with a bufferHead index and advances it in O(1) instead of shifting. readHeader() reads from bufferHead as well. Consumed slots are released for GC, and the array is compacted:

  • fully drained (bufferHead === bufferList.length) → reset in O(1) (the common terminal state), or
  • once the consumed prefix grows past a small threshold → splice() it off (amortized O(1) per consumed chunk).

This mirrors the index-pointer approach already used in transformStreamWithRouter for the same reason.

Why

Same hot path as the sibling zero-copy PR: decoding streamed server-function responses and RawStream payloads. The O(n²) bites specifically when one frame spans many buffered chunks.

Standalone micro-benchmark (Node, median of 12 runs), draining N buffered chunks:

Chunks shift() head pointer Speedup
200 26.7 ms 2.5 ms 10.6x
1000 173 ms 15 ms 11.5x

Tests

  • Existing frame-decoder suite passes.
  • Added two tests for the changed paths:
    • a 200-byte CHUNK payload delivered one byte at a time (forces the header slow path + many whole-chunk consumptions + the fully-drained reset),
    • 100-byte frames fed in 7-byte reads that never align with frame boundaries, so the head pointer climbs past the compaction threshold repeatedly (exercises the splice() prefix drop).
nx run @tanstack/start-client-core:test:unit   -> 20 passed
nx run @tanstack/start-client-core:test:types  -> no errors

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Includes a changeset (@tanstack/start-client-core patch).

Summary by CodeRabbit

  • Performance

    • Optimized frame decoder to more efficiently handle large frames when data arrives as multiple small reads, reducing processing overhead.
  • Tests

    • Added test coverage for frame decoder with fragmented input streams and non-aligned chunk boundaries.

The frame decoder dropped consumed chunks from its buffer with
bufferList.shift(), which is O(n). When a single large frame (e.g. a big
RawStream payload) is assembled from many small network reads, the
extract loop calls shift() once per chunk, making reassembly O(n^2).

Track the first un-consumed chunk with a head pointer and advance it in
O(1) instead of shifting. Consumed slots are released for GC, and the
buffer is compacted when fully drained (O(1) reset) or once the consumed
prefix grows past a small threshold (amortized O(1) per chunk).

A micro-benchmark draining 1000 small chunks is ~11x faster.
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

createFrameDecoder in frame-decoder.ts now tracks the first unconsumed chunk via a bufferHead index instead of calling bufferList.shift(). Both paths of readHeader and the extractFlattened function are updated to use this pointer, with periodic list compaction. Two new tests cover one-byte-at-a-time and 7-byte-misaligned delivery, and a patch changeset entry is added.

Changes

Frame decoder O(1) buffer optimization

Layer / File(s) Summary
bufferHead pointer and extractFlattened rewrite
packages/start-client-core/src/client-rpc/frame-decoder.ts
Adds bufferHead to the parsing loop. Updates readHeader's fast path to read from bufferList[bufferHead] and its slow-path loop to iterate from bufferHead. Rewrites extractFlattened to advance bufferHead on full chunk consumption, replace partially consumed chunks with subarrays, and splice the consumed prefix after a compaction threshold.
Slow-path and misaligned-chunk tests, changeset
packages/start-client-core/tests/frame-decoder.test.ts, .changeset/perf-frame-decoder-index-pointer.md
Adds a test that feeds a CHUNK payload one byte per enqueue and reassembles it, adds a test that feeds multiple JSON frames in 7-byte misaligned reads and verifies ordered decoding, and records a patch changeset entry for the optimization.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 No more shifting the pile — just a pointer so neat,
I hop to the head where the fresh chunks I meet.
O(n²) once loomed like a very tall stack,
Now O(1) each step, no slow tumble back.
The frames decode swiftly, the buffers stay trim —
A rabbit's best run, fast and light on a whim! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely captures the main performance optimization: replacing an O(n) shift operation with an O(1) head pointer approach in the client frame decoder.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/start-client-core/tests/frame-decoder.test.ts (1)

567-567: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use braces for one-line control statements in tests.

Line 567, Line 600-601, and Line 641 use single-line bodies without braces, which violates the project rule for TS/JS control flow.

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Always use curly braces for if, else, loops, and similar control statements. Never write one-line bodies like if (foo) x = 1."

Suggested patch
-      for (let i = 0; i < payload.length; i++) payload[i] = (i * 7) % 256
+      for (let i = 0; i < payload.length; i++) {
+        payload[i] = (i * 7) % 256
+      }

       while (true) {
         const { done, value } = await rawReader.read()
-        if (done) break
-        if (value) received.push(...value)
+        if (done) {
+          break
+        }
+        if (value) {
+          received.push(...value)
+        }
       }

       while (true) {
         const { done, value } = await reader.read()
-        if (done) break
+        if (done) {
+          break
+        }
         received.push(value)
       }

Also applies to: 600-601, 641-641

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-client-core/tests/frame-decoder.test.ts` at line 567, The for
loop on line 567 with body payload[i] = (i * 7) % 256 uses a single-line body
without braces, violating the project's coding guidelines. Add curly braces
around the loop body to properly format it. Additionally, apply the same fix to
the other single-line control statements mentioned at lines 600-601 and 641 by
wrapping their bodies in curly braces.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/start-client-core/tests/frame-decoder.test.ts`:
- Line 567: The for loop on line 567 with body payload[i] = (i * 7) % 256 uses a
single-line body without braces, violating the project's coding guidelines. Add
curly braces around the loop body to properly format it. Additionally, apply the
same fix to the other single-line control statements mentioned at lines 600-601
and 641 by wrapping their bodies in curly braces.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9195414-0588-45d1-b334-36aea734698b

📥 Commits

Reviewing files that changed from the base of the PR and between 279a849 and 38c9d48.

📒 Files selected for processing (3)
  • .changeset/perf-frame-decoder-index-pointer.md
  • packages/start-client-core/src/client-rpc/frame-decoder.ts
  • packages/start-client-core/tests/frame-decoder.test.ts

@nx-cloud

nx-cloud Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 38c9d48

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 14m View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 18s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-22 11:15:39 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 22, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7663

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7663

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7663

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7663

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7663

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7663

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7663

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7663

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7663

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7663

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7663

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7663

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7663

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7663

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7663

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7663

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7663

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7663

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7663

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7663

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7663

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7663

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7663

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7663

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7663

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7663

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7663

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7663

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7663

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7663

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7663

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7663

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7663

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7663

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7663

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7663

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7663

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7663

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7663

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7663

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7663

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7663

commit: 38c9d48

@codspeed-hq

codspeed-hq Bot commented Jun 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 142 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem serialization-payload (solid) 6.9 MB 7.4 MB -5.96%
Memory mem aborted-requests (solid) 1.8 MB 1.6 MB +12.59%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing anonrig:perf/frame-decoder-index-pointer (38c9d48) with main (f23ed0f)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (279a849) during the generation of this report, so f23ed0f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Sheraff

Sheraff commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Can you fix the conflicts on this one? And then I'll merge it

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.

2 participants