unsloth/studio
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
..
backend fix(rocm): prefer system LLVM runtime on native Linux (#7448) 2026-07-28 06:19:29 -07:00
frontend fix(studio): stop re-tokenizing the whole code block on every frame while streaming (#7537) 2026-07-28 06:30:28 -07:00
src-tauri Studio: keep grouped Python scripts visible and save them natively (#7528) 2026-07-28 05:41:36 -07:00
__init__.py Final cleanup 2026-03-12 18:28:04 +00:00
install_llama_prebuilt.py fix(rocm): prefer system LLVM runtime on native Linux (#7448) 2026-07-28 06:19:29 -07:00
install_manifest.py Fix Windows no-torch setup (#7511) 2026-07-28 05:54:25 -07:00
install_node_prebuilt.py Let a decode failure degrade instead of escaping a fail-closed helper (#7487) 2026-07-27 03:26:08 -07:00
install_python_stack.py Fix Windows no-torch setup (#7511) 2026-07-28 05:54:25 -07:00
install_whisper_prebuilt.py Studio whisper: pair slim bundles on the ggml commit, not the full llama tag (#7381) 2026-07-23 20:18:36 -07:00
LICENSE.AGPL-3.0 Add AGPL-3.0 license to studio folder 2026-03-09 19:36:25 +00:00
MCP.md Replace standalone Studio wording with Unsloth (#7221) 2026-07-19 00:47:04 -07:00
node_prebuilt_pins.json Pin isolated Node.js installer to committed sha256 digests (#6625) 2026-06-24 05:47:58 -07:00
package-lock.json ci: advisory lockfile supply-chain audit (no install-script changes) (#5604) 2026-05-19 05:56:56 -07:00
package.json ci: advisory lockfile supply-chain audit (no install-script changes) (#5604) 2026-05-19 05:56:56 -07:00
prebuilt_core.py Let a decode failure degrade instead of escaping a fail-closed helper (#7487) 2026-07-27 03:26:08 -07:00
setup.bat Final cleanup 2026-03-12 18:28:04 +00:00
setup.ps1 Fix Windows no-torch setup (#7511) 2026-07-28 05:54:25 -07:00
setup.sh Studio: detect an interrupted dependency install instead of launching a backend that cannot import (#7492) 2026-07-28 10:57:20 +02:00
Unsloth_Studio_Colab.ipynb fix(studio/colab): restore blank Colab iframe embed (#7344) (#7349) 2026-07-24 02:23:24 -07:00