Skip to content

[Bug]: Heavy CPU Usage on linux #4074

Description

@mudit-loya

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/desktop

Steps to reproduce

Peak t3code CPU% (renderer + GPU helper) observed in htop while streaming a reply of comparable length, sampled twice per scenario:Scenario Peak CPU%Long chat (many prior messages) 76%, 65%Fresh chat (no history) 35%
Same model, same output speed, similar reply length. The only independent variable is the amount of already-rendered conversation text —
isolating message length as the driver. This is the signature of a full re-parse on every delta.
Secondary observation: the fresh-chat baseline (~35%) is itself high for streaming a few paragraphs, implying a per-frame rendering cost separate
from the length-scaling. The length-scaling (76% vs 35% gap) is the primary target; the baseline is a smaller separate matter.

Expected behavior

Long-chat streaming CPU should drop from ~76% toward the fresh-chat baseline (~35–40%), and — more importantly — stop scaling with
conversation length, so very long chats no longer approach a maxed core.

Fenced code blocks containing blank lines (must not split mid-fence)
Task-list checkbox offsets (block-relative + block start-offset)
Footnotes / reference-style links that span blocks (GFM)
Tables and nested lists straddling block boundaries
Verify against existing apps/web/src/markdown-list-indentation.test.tsx

Actual behavior

While an agent response streams, T3Code re-parses the entire accumulated message through the markdown pipeline on every token delta. This
is O(n2) work over the length of a stream: cost grows with how much text is already rendered, so long chats pin ~1 CPU core and cause visible UI
lag (and, on a shared X11 pipeline, lag in other apps too).
This is a code-level inefficiency present for all users; low-RAM / X11 / iGPU setups just surface it most sharply.

Root cause (source references)
File: apps/web/src/components/ChatMarkdown.tsx
Full-message re-parse per delta — line ~1565 tsx <ReactMarkdown ...>{text} text is the full accumulated
message. Each streamed delta changes text, so react-markdown (^10.1.0) re-runs the whole remark-gfm/remark-breaks → rehype →
React pipeline over the entire message every time. Over a stream of N deltas this is Σ(1..N) ≈ O(n2) total parse work.
Components map rebuilt per delta — line ~1348 tsx const markdownComponents = useMemo(() => ({ ... }), [
..., isStreaming, text, ... // text + isStreaming in deps ]); Because text (and isStreaming) are in the dependency
array, the entire components mapping is rebuilt on every delta, defeating subtree reuse even for markdown blocks that did not change.
text is in deps because the li renderer computes task-list checkbox positions as offsets into the full message
(findTaskListMarkerOffset(text, listItemStart)). This coupling is what makes the fix non-trivial (see below).

Impact

Major degradation or frequent failure

Version or commit

No response

Environment

No response

Logs or stack traces

Screenshots, recordings, or supporting files

No response

Workaround


Proposed fix
Standard streaming-markdown pattern (as used by Vercel streamdown / AI SDK markdown chunking):
Block-split the message. Add a fence-aware splitMarkdownBlocks(text) that splits on blank lines but treats fenced code blocks (``` /
~~) as atomic, returning blocks with stable keys and their start-offset into the full text. (50–100 LOC + unit tests — this is the core new
piece.)
Memoized per-block component. Render each block via a memo’d MarkdownBlock. During streaming, only the final (incomplete) block’s
text changes, so only it re-parses; all prior blocks hit the memo and skip work. Turns the per-delta cost from O(message) to O(last-block).
Stabilize markdownComponents. Remove text/isStreaming from the deps by passing the block start-offset into each block (task-list
offsets become block-relative + offset), and reading mutable data (file-link metadata, handlers) via ref/context rather than closure over
text.
Public API of ChatMarkdown stays the same — the ~5 call sites in MessagesTimeline.tsx / ChatView.tsx are unaffected.
Cheaper tier-1 alternative
Just stabilize markdownComponents (step 3) without block-splitting — ~50 LOC, likely recovers a meaningful fraction with minimal risk. Good
fallback if maintainers prefer minimal surface area.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.needs-triageIssue needs maintainer review and initial categorization.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions