mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): reduce memory retained per rendered message
Markdown holds its parsed tokens weakly: a token tree is about ten times its source, and every transcript message keeps a Markdown component. The tokens still survive bursts of re-renders such as theme previews. Markdown, Text, and Box flatten their cached lines, which V8 otherwise keeps as trees of the concatenated parts. A long assistant message keeps about a fifth of the heap.
This commit is contained in:
parent
5806068c26
commit
54c19a2529
5 changed files with 23 additions and 7 deletions
|
|
@ -9,6 +9,7 @@
|
|||
### Fixed
|
||||
|
||||
- Fixed color bleeding past mouse selections and search highlights in fullscreen mode when a styled token ends at the highlight boundary ([#10169](https://github.com/earendil-works/pi/issues/10169))
|
||||
- Fixed memory retained per rendered message: `Markdown` holds its parsed tokens weakly, and `Markdown`, `Text`, and `Box` flatten their cached lines. A long assistant message keeps about a fifth of the heap it kept before.
|
||||
|
||||
## [0.99.2] - 2026-09-30
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { type Component, dispatchMouseEvent, type TuiMouseDispatchResult, type TuiMouseEvent } from "../tui.ts";
|
||||
import { visibleWidth } from "../utils.ts";
|
||||
import { flattenLines, visibleWidth } from "../utils.ts";
|
||||
|
||||
type RenderCache = {
|
||||
childLines: string[];
|
||||
|
|
@ -151,6 +151,7 @@ export class Box implements Component {
|
|||
}
|
||||
|
||||
// Update cache
|
||||
flattenLines(result);
|
||||
this.cache = { childLines, width, bgSample, lines: result };
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Marked, type Token, Tokenizer, type TokenizerExtension, type Tokens } f
|
|||
import { renderLatex } from "../latex.ts";
|
||||
import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.ts";
|
||||
import type { Component } from "../tui.ts";
|
||||
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.ts";
|
||||
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.ts";
|
||||
|
||||
const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
||||
|
||||
|
|
@ -246,8 +246,10 @@ export class Markdown implements Component {
|
|||
private cachedText?: string;
|
||||
private cachedWidth?: number;
|
||||
private cachedLines?: string[];
|
||||
// Parsed tokens depend only on the source, so they survive theme and width invalidation.
|
||||
private cachedTokens?: { source: string; tokens: Token[] };
|
||||
// Parsed tokens depend only on the source, so they survive theme and width invalidation. Held weakly: a token tree is
|
||||
// about ten times the size of its source, and every message of a long transcript keeps a Markdown component. The
|
||||
// tokens survive a burst of re-renders, such as a theme preview, and are collected afterwards.
|
||||
private cachedTokens?: WeakRef<{ source: string; tokens: Token[] }>;
|
||||
|
||||
constructor(
|
||||
text: string,
|
||||
|
|
@ -300,11 +302,12 @@ export class Markdown implements Component {
|
|||
const normalizedText = text.replace(/\t/g, " ");
|
||||
|
||||
// Parse markdown to HTML-like tokens
|
||||
let tokens = this.cachedTokens?.source === normalizedText ? this.cachedTokens.tokens : undefined;
|
||||
const cached = this.cachedTokens?.deref();
|
||||
let tokens = cached?.source === normalizedText ? cached.tokens : undefined;
|
||||
if (!tokens) {
|
||||
tokens = markdownParser.lexer(normalizedText);
|
||||
trimPartialClosingFences(tokens);
|
||||
this.cachedTokens = { source: normalizedText, tokens };
|
||||
this.cachedTokens = new WeakRef({ source: normalizedText, tokens });
|
||||
}
|
||||
|
||||
// Convert tokens to styled terminal output
|
||||
|
|
@ -365,6 +368,7 @@ export class Markdown implements Component {
|
|||
|
||||
// Combine top padding, content, and bottom padding
|
||||
const result = emptyLines.concat(contentLines, emptyLines);
|
||||
flattenLines(result);
|
||||
|
||||
// Update cache
|
||||
this.cachedText = this.text;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from "../tui.ts";
|
||||
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.ts";
|
||||
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* Text component - displays multi-line text with word wrapping
|
||||
|
|
@ -96,6 +96,7 @@ export class Text implements Component {
|
|||
}
|
||||
|
||||
const result = [...emptyLines, ...contentLines, ...emptyLines];
|
||||
flattenLines(result);
|
||||
|
||||
// Update cache
|
||||
this.cachedText = this.text;
|
||||
|
|
|
|||
|
|
@ -888,6 +888,15 @@ function splitIntoTokensWithAnsi(text: string): string[] {
|
|||
* @param width - Maximum visible width per line
|
||||
* @returns Array of wrapped lines (NOT padded to width)
|
||||
*/
|
||||
/**
|
||||
* Flatten cached lines. V8 keeps a string built by concatenation as a tree of its parts until something reads it
|
||||
* whole, and a cached line kept as such a tree retains several times its own size. Converting a string to a number
|
||||
* reads it whole, so V8 flattens it in place; the strings' values do not change.
|
||||
*/
|
||||
export function flattenLines(lines: readonly string[]): void {
|
||||
for (const line of lines) Number(line);
|
||||
}
|
||||
|
||||
export function wrapTextWithAnsi(text: string, width: number): string[] {
|
||||
if (!text) {
|
||||
return [""];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue