perf(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks
, '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(chat): keep the virtual list at full framerate in long sessions - #286

Merged
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list
Aug 26, 2026
Merged

perf(chat): keep the virtual list at full framerate in long sessions#286
Tryanks merged 3 commits into
mainfrom
perf/chatview-virtual-list

Conversation

@Tryanks

Copy link
Copy Markdown
Owner

Problem

Perceptible jank and low framerate in ChatView, worst in long sessions and during long streaming replies. Three synchronous cost centers were responsible:

  1. MarkdownState::push_str reparsed the whole document on every streamed delta — O(len) per append, quadratic over a long reply, all on the UI thread.
  2. Every store notification re-hashed every timeline entry (index_turns) and cloned a String id per markdown entry (sync_markdown_residency) — O(session) per notification, growing with chat length.
  3. Scrolling into an evicted region or opening a long session parsed large markdown documents synchronously inside the frame.

Changes

  • Incremental streaming parse — the parser records root-block source offsets; an append reparses only from the last root block's start and splices the tail onto the reused prefix. Appended link reference definitions (which can restyle earlier blocks) conservatively fall back to a full reparse. Streaming bench (50KB in 100B deltas): ~151ms → ~7ms.
  • Incremental turn indexingTurnIndexCache finds the divergence point via Arc::ptr_eq prefix scan and re-hashes only affected tail turns (1 turn per streaming tick on a 200-turn timeline, asserted in tests). The residency walk pre-filters by the same keep/pinned window decide() uses before allocating ids.
  • Async large-markdown builds — texts over 4KiB parse on the background executor while the existing plain-text fallback renders; completion inserts the state, remeasures the turn, and notifies. Generation + pending-map guards handle in-flight updates, evictions, and session switches. Streaming replies start under the threshold and stay on the synchronous incremental path.

Verification

  • Equivalence tests: streamed parse ≡ full parse (chunk sizes 1/7/64 over a corpus incl. setext, nested lists, unclosed fences, tables, link refs); incremental indexing ≡ full index_turns across append/replace/toggle/reset/rewind; filtered residency decisions ≡ unfiltered.
  • Race tests: in-flight text update lands with latest text; session switch does not resurrect stale builds; no duplicate jobs; small texts stay synchronous.
  • cargo fmt --all --check, cargo clippy --workspace --all-targets --locked -- -D warnings, cargo test --workspace --locked all pass locally (280 UI tests).

…ping incremental
index_turns re-hashed every timeline entry on every store notification,
and sync_markdown_residency cloned a String id per markdown entry per
call - both O(session) costs that grew with chat length during
streaming. TurnIndexCache now reuses the unchanged prefix via
Arc::ptr_eq divergence scan and re-hashes only the affected tail turns;
the residency walk pre-filters entries by the same keep/pinned window
decide() uses before allocating ids.
push_str reparsed the whole document on every streamed delta - O(len)
per append, quadratic over a long reply, all on the UI thread. The
parser now records root-block source offsets; an append reparses from
the last root block's start and splices the tail onto the reused
prefix. Appended link reference definitions (which can restyle earlier
blocks) conservatively fall back to a full reparse.
Streaming bench (50KB in 100B deltas): ~151ms -> ~7ms.
Scrolling into an evicted region or opening a long session parsed the
full document synchronously inside the frame. Texts over 4KiB now parse
on the background executor while the existing plain-text fallback
renders; completion inserts the state, remeasures the turn, and
notifies. In-flight updates reconcile through the incremental append
path (or re-kick on edits), and generation + pending-map guards keep
evictions and session switches from resurrecting stale work. Streaming
replies start under the threshold and stay on the synchronous
incremental path.
@Tryanks
Tryanks merged commit ebc252c into mainAug 26, 2026
3 checks passed
@Tryanks
Tryanks deleted the perf/chatview-virtual-list branch August 26, 2026 07:11
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

@Tryanks