Reduce terminal and dashboard memory footprint
Frontend terminal: - Share single TextDecoder across all instances (was one per terminal) - Write binary WS frames as Uint8Array directly to ghostty-web, avoiding intermediate string allocation from TextDecoder.decode() - Reduce default scrollback to 200 on mobile (was 1000; Safari mobile has ~300MB budget across all tabs) - Pause heartbeat watchdog when tab is hidden to reduce WS traffic and message queue growth for background terminals Dashboard screenshots: - Revoke old object URL before creating new one (reduces peak memory by not having two decoded bitmaps alive simultaneously) - Re-lookup card from cardsBySlug in fetch callback to avoid orphaned blob URLs when renderTiles() runs during in-flight requests - Remove thumbnailCache writes (was redundant with activeObjectURLBySlug) Server: - Halve replay buffer from 256KB to 128KB per session
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ package webterm
|
|||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
const replayBufferSize = 256 * 1024
|
const replayBufferSize = 128 * 1024
|
||||||
|
|
||||||
type ReplayBuffer struct {
|
type ReplayBuffer struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|||||||
+4
-6
@@ -1270,7 +1270,6 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
screenshotRequestInFlight = true;
|
screenshotRequestInFlight = true;
|
||||||
const img = card.img;
|
|
||||||
const url = '/screenshot.svg?route_key=' + encodeURIComponent(slug);
|
const url = '/screenshot.svg?route_key=' + encodeURIComponent(slug);
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 5000);
|
const timeout = setTimeout(() => controller.abort(), 5000);
|
||||||
@@ -1290,14 +1289,13 @@ func (s *LocalServer) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
.then((blob) => {
|
.then((blob) => {
|
||||||
if (!blob) return;
|
if (!blob) return;
|
||||||
|
const currentCard = cardsBySlug[slug];
|
||||||
|
if (!currentCard || !currentCard.img) return;
|
||||||
const previous = activeObjectURLBySlug[slug];
|
const previous = activeObjectURLBySlug[slug];
|
||||||
|
if (previous) URL.revokeObjectURL(previous);
|
||||||
const objectURL = URL.createObjectURL(blob);
|
const objectURL = URL.createObjectURL(blob);
|
||||||
activeObjectURLBySlug[slug] = objectURL;
|
activeObjectURLBySlug[slug] = objectURL;
|
||||||
img.src = objectURL;
|
currentCard.img.src = objectURL;
|
||||||
thumbnailCache[slug] = { src: objectURL, updatedAt: Date.now() };
|
|
||||||
if (previous) {
|
|
||||||
URL.revokeObjectURL(previous);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -26,6 +26,9 @@ async function getSharedGhostty(): Promise<Ghostty> {
|
|||||||
return sharedGhostty;
|
return sharedGhostty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shared TextDecoder (stateless for UTF-8, safe to share) */
|
||||||
|
const sharedTextDecoder = new TextDecoder();
|
||||||
|
|
||||||
/** Default font stack - prefers system monospace, falls back through programming fonts */
|
/** Default font stack - prefers system monospace, falls back through programming fonts */
|
||||||
const DEFAULT_FONT_FAMILY =
|
const DEFAULT_FONT_FAMILY =
|
||||||
'ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", ' +
|
'ui-monospace, "SFMono-Regular", "FiraCode Nerd Font", "FiraMono Nerd Font", ' +
|
||||||
@@ -660,7 +663,6 @@ class WebTerminal {
|
|||||||
private terminal: Terminal;
|
private terminal: Terminal;
|
||||||
private fitAddon: FitAddon;
|
private fitAddon: FitAddon;
|
||||||
private socket: WebSocket | null = null;
|
private socket: WebSocket | null = null;
|
||||||
private readonly textDecoder = new TextDecoder();
|
|
||||||
private element: HTMLElement;
|
private element: HTMLElement;
|
||||||
private wsUrl: string;
|
private wsUrl: string;
|
||||||
private reconnectAttempts = 0;
|
private reconnectAttempts = 0;
|
||||||
@@ -741,10 +743,11 @@ class WebTerminal {
|
|||||||
const fontFamily = config.fontFamily?.trim() || DEFAULT_FONT_FAMILY;
|
const fontFamily = config.fontFamily?.trim() || DEFAULT_FONT_FAMILY;
|
||||||
const fontSize = config.fontSize ?? 16;
|
const fontSize = config.fontSize ?? 16;
|
||||||
|
|
||||||
|
const defaultScrollback = isMobileDevice() ? 200 : 1000;
|
||||||
const options: ITerminalOptions = {
|
const options: ITerminalOptions = {
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
scrollback: config.scrollback ?? 1000,
|
scrollback: config.scrollback ?? defaultScrollback,
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
theme: themeToUse,
|
theme: themeToUse,
|
||||||
@@ -905,7 +908,12 @@ class WebTerminal {
|
|||||||
|
|
||||||
// Focus terminal when returning to the tab
|
// Focus terminal when returning to the tab
|
||||||
this.addTrackedListener(document, "visibilitychange", () => {
|
this.addTrackedListener(document, "visibilitychange", () => {
|
||||||
if (!document.hidden) {
|
if (document.hidden) {
|
||||||
|
this.stopHeartbeatWatchdog();
|
||||||
|
} else {
|
||||||
|
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||||
|
this.startHeartbeatWatchdog();
|
||||||
|
}
|
||||||
restoreFocus();
|
restoreFocus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1634,9 +1642,8 @@ class WebTerminal {
|
|||||||
private handleMessage(data: string | ArrayBuffer | Blob): void {
|
private handleMessage(data: string | ArrayBuffer | Blob): void {
|
||||||
this.lastMessageAt = Date.now();
|
this.lastMessageAt = Date.now();
|
||||||
if (data instanceof ArrayBuffer) {
|
if (data instanceof ArrayBuffer) {
|
||||||
// Binary data - write directly to terminal
|
// Binary data - write directly to terminal as Uint8Array (avoids string allocation)
|
||||||
const text = this.textDecoder.decode(data);
|
this.terminal.write(new Uint8Array(data));
|
||||||
this.terminal.write(text);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data instanceof Blob) {
|
if (data instanceof Blob) {
|
||||||
|
|||||||
Reference in New Issue
Block a user