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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

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

Closed
anonrig wants to merge 1 commit into
mainfrom
perf/frame-decoder-index-pointer
Closed

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

Conversation

@anonrig

Copy link
Copy Markdown
Owner

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 existing 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:

Chunksshift()head pointerSpeedup
20026.7 ms2.5 ms10.6x
1000173 ms15 ms11.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).
test:unit ✓ 20 passed
test:types ✓ no errors
eslint ✓ frame-decoder.ts clean (no new problems vs main)

Notes

  • Independent of the sibling zero-copy PR. Both touch extractFlattened, so whichever merges second will need a trivial rebase.
  • Pre-existing import/order lint errors in src/client/hydrateStart.ts (virtual-module imports) are unrelated to this change and present on main.

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.
@anonrig

Copy link
Copy Markdown
OwnerAuthor

Superseded by upstream PR TanStack#7663 (opened against TanStack/router).

@anonriganonrig closed this Jun 21, 2026
Sign up for freeto 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

@anonrig