unsloth/studio/frontend
Michael Han 9e568c14e6
fix(studio): stop re-tokenizing the whole code block on every frame while streaming (#7537)
* fix(studio): reuse cached tokens while highlighting streaming code blocks

A streaming fence re-enters highlight() every animation frame with the whole
block, so Shiki re-tokenizes it from scratch each time: O(length) per frame and
O(length^2) over the message. One generation made 808 highlight() calls and
tokenized 5.5MB of text to render a 13.5KB block, putting ~50% of the renderer
main thread in the TextMate tokenizer.

Blocks under 2000 chars are unchanged. Above that, a growing fence reuses the
tokens from the last real tokenization and appends the new tail unstyled, with
a full re-tokenize at most every 250ms.

* fix(studio): render the streamed tail unstyled and always converge

Two defects found while property-testing the reuse path:

- plainLine() spread the template token, so newly streamed lines inherited the
  first token's colour instead of the default foreground. Emit a bare token.
- A reused result could be the final one if the caller stopped re-rendering,
  leaving the tail permanently unstyled. Schedule a trailing re-tokenize so a
  reused run always converges.

* fix(studio): key the highlight cache per fence and keep tokens paired with code

Review found four real defects in the previous approach:

- entry.code advanced at dispatch time while entry.result still held the older
  tokens, so a reuse could slice one against the other and drop text from the
  cached run's final line.
- A finished fence re-rendered with identical code re-dispatched every frame,
  keeping the per-frame cost for the rest of the stream.
- All fences of one language shared a single entry, so sibling fences evicted
  each other and both were fully tokenized on every render.
- An overdue trailing timer could dispatch stale code after a newer dispatch.

Cache is now one slot per fence, matched by longest prefix. code and result
only ever move together, an exact match is served straight from cache, and a
direct dispatch or a slot eviction cancels any pending trailing refresh.

* Studio: adopt synchronous highlight results and use a monotonic throttle

@streamdown/code answers out of its own cache synchronously and never invokes
the callback in that case. dispatch() ignored that return value, so the slot
kept pointing at the older tokens. On the trailing refresh, where nothing else
consumes the return, that left the fence showing its unstyled tail until an
unrelated remount. Adopt the synchronous result on both paths and hand it to
the pending callback.

Drive the throttle off performance.now(). Date.now() is wall clock, so a
backward step from an NTP correction or a resume from sleep makes elapsed
negative, which pins the reuse branch on and schedules the trailing refresh by
the size of the step.

* Tighten code-plugin comments

---------

Co-authored-by: shimmyshimmer <info@unsloth.ai>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-07-28 06:30:28 -07:00
..
public Add interactive Agents command builder (#7312) 2026-07-26 17:09:19 -07:00
src fix(studio): stop re-tokenizing the whole code block on every frame while streaming (#7537) 2026-07-28 06:30:28 -07:00
tests Studio: preserve durations across reasoning blocks (#7520) 2026-07-28 04:53:26 -07:00
.gitignore perf(studio): upgrade to Vite 8 + auto-install bun for faster frontend builds (#4522) 2026-03-25 04:27:41 -07:00
.gitkeep add studio root folder 2026-02-02 09:14:35 +00:00
.npmrc Replace standalone Studio wording with Unsloth (#7221) 2026-07-19 00:47:04 -07:00
biome.json feat: add seed dataset support with configuration, preview, and builder utilities 2026-02-14 18:44:38 +01:00
components.json add studio root folder 2026-02-02 09:14:35 +00:00
data-designer.openapi (1).yaml save and import, and fixes 2026-02-04 14:32:49 +01:00
eslint.config.js Final cleanup 2026-03-12 18:28:04 +00:00
index.html Unsloth: appearance palettes, customization options, and control restyle (#7077) 2026-07-14 05:12:08 -07:00
package-lock.json Unsloth: appearance palettes, customization options, and control restyle (#7077) 2026-07-14 05:12:08 -07:00
package.json Studio: preserve durations across reasoning blocks (#7520) 2026-07-28 04:53:26 -07:00
tsconfig.app.json chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
tsconfig.json chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
tsconfig.node.json chore(studio/frontend): normalize line endings to LF (#6012) 2026-06-12 03:51:59 -07:00
tsconfig.test.json Studio: preserve durations across reasoning blocks (#7520) 2026-07-28 04:53:26 -07:00
vite.config.ts Isolate Studio PostCSS configuration (#7513) 2026-07-28 03:14:35 -07:00