Skip to content

Commit ce6fd4d

Browse files
Buffer logs to not overflow xterm (#20065)
* Lock logs replaying to prevent overflowing xterm * Chunk on the workspace logs side * Revert to previous workspacelogs event emitter type * remove unused dep * fix import order * Only change `isWriting` when there are logs
1 parent 21bb9ad commit ce6fd4d

File tree

4 files changed

+44
-20
lines changed

4 files changed

+44
-20
lines changed

components/dashboard/src/components/PrebuildLogs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import EventEmitter from "events";
8-
import React, { Suspense, useCallback, useEffect, useState } from "react";
8+
import React, { Suspense, useCallback, useEffect, useMemo, useState } from "react";
99
import {
1010
DisposableCollection,
1111
WorkspaceImageBuild,
@@ -37,7 +37,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
3737
| undefined
3838
>();
3939
const [error, setError] = useState<Error | undefined>();
40-
const [logsEmitter] = useState(new EventEmitter());
40+
const logsEmitter = useMemo(() => new EventEmitter(), []);
4141
const [prebuild, setPrebuild] = useState<Prebuild | undefined>();
4242

4343
const handlePrebuildUpdate = useCallback(

components/dashboard/src/components/WorkspaceLogs.tsx

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ const lightTheme: ITheme = {
2525
selectionBackground: "#add6ff80", // https://github.com/gitpod-io/gitpod-vscode-theme/blob/6fb17ba8915fcd68fde3055b4bc60642ce5eed14/themes/gitpod-light-color-theme.json#L15
2626
};
2727

28-
export interface WorkspaceLogsProps {
28+
export interface Props {
2929
logsEmitter: EventEmitter;
3030
errorMessage?: string;
3131
classes?: string;
3232
xtermClasses?: string;
3333
}
3434

35-
export default function WorkspaceLogs(props: WorkspaceLogsProps) {
35+
const MAX_CHUNK_SIZE = 1024 * 4; // 4KB
36+
37+
export default function WorkspaceLogs({ logsEmitter, errorMessage, classes, xtermClasses }: Props) {
3638
const xTermParentRef = useRef<HTMLDivElement>(null);
3739
const terminalRef = useRef<Terminal>();
3840
const fitAddon = useMemo(() => new FitAddon(), []);
@@ -54,17 +56,37 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
5456
terminal.loadAddon(fitAddon);
5557
terminal.open(xTermParentRef.current);
5658

57-
const logListener = (logs: string) => {
58-
if (terminal && logs) {
59-
terminal.write(logs);
59+
let logBuffer = "";
60+
let isWriting = false;
61+
62+
const processNextLog = () => {
63+
if (isWriting || logBuffer.length === 0) return;
64+
65+
const logs = logBuffer.slice(0, MAX_CHUNK_SIZE);
66+
logBuffer = logBuffer.slice(logs.length);
67+
if (logs) {
68+
isWriting = true;
69+
terminal.write(logs, () => {
70+
isWriting = false;
71+
processNextLog();
72+
});
6073
}
6174
};
6275

76+
const logListener = (logs: string) => {
77+
if (!logs) return;
78+
79+
logBuffer += logs;
80+
processNextLog();
81+
};
82+
6383
const resetListener = () => {
6484
terminal.clear();
85+
logBuffer = "";
86+
isWriting = false;
6587
};
6688

67-
const emitter = props.logsEmitter.on("logs", logListener);
89+
const emitter = logsEmitter.on("logs", logListener);
6890
emitter.on("reset", resetListener);
6991
fitAddon.fit();
7092

@@ -74,7 +96,7 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
7496
emitter.removeListener("reset", resetListener);
7597
};
7698
// eslint-disable-next-line react-hooks/exhaustive-deps
77-
}, [props.logsEmitter]);
99+
}, [logsEmitter]);
78100

79101
const resizeDebounced = debounce(
80102
() => {
@@ -95,11 +117,11 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
95117
}, []);
96118

97119
useEffect(() => {
98-
if (terminalRef.current && props.errorMessage) {
99-
terminalRef.current.write(`\r\n\u001b[38;5;196m${props.errorMessage}\u001b[0m\r\n`);
120+
if (terminalRef.current && errorMessage) {
121+
terminalRef.current.write(`\r\n\u001b[38;5;196m${errorMessage}\u001b[0m\r\n`);
100122
}
101123
// eslint-disable-next-line react-hooks/exhaustive-deps
102-
}, [terminalRef.current, props.errorMessage]);
124+
}, [terminalRef.current, errorMessage]);
103125

104126
useEffect(() => {
105127
if (!terminalRef.current) {
@@ -112,12 +134,12 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
112134
return (
113135
<div
114136
className={cn(
115-
props.classes || "mt-6 h-72 w-11/12 lg:w-3/5 rounded-xl overflow-hidden",
137+
classes || "mt-6 h-72 w-11/12 lg:w-3/5 rounded-xl overflow-hidden",
116138
"bg-pk-surface-secondary relative text-left",
117139
)}
118140
>
119141
<div
120-
className={cn(props.xtermClasses || "absolute top-0 left-0 bottom-0 right-0 m-6")}
142+
className={cn(xtermClasses || "absolute top-0 left-0 bottom-0 right-0 m-6")}
121143
ref={xTermParentRef}
122144
></div>
123145
</div>

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
99
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
1010
import EventEmitter from "events";
1111
import * as queryString from "query-string";
12-
import React, { Suspense, useEffect, useState } from "react";
12+
import React, { Suspense, useEffect, useMemo } from "react";
1313
import { v4 } from "uuid";
1414
import Arrow from "../components/Arrow";
1515
import ContextMenu from "../components/ContextMenu";
@@ -760,7 +760,7 @@ interface ImageBuildViewProps {
760760
}
761761

762762
function ImageBuildView(props: ImageBuildViewProps) {
763-
const [logsEmitter] = useState(new EventEmitter());
763+
const logsEmitter = useMemo(() => new EventEmitter(), []);
764764

765765
useEffect(() => {
766766
let registered = false;

components/dashboard/src/utils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,9 @@ export class ReplayableEventEmitter<EventTypes extends EventMap> extends EventEm
173173
on<K extends keyof EventTypes>(event: K, listener: (...args: EventTypes[K]) => void): this;
174174
on(event: string | symbol, listener: (...args: any[]) => void): this {
175175
const eventName = event as keyof EventTypes;
176-
if (this.eventLog[eventName]) {
177-
for (const args of this.eventLog[eventName]!) {
176+
const eventLog = this.eventLog[eventName];
177+
if (eventLog) {
178+
for (const args of eventLog) {
178179
listener(...args);
179180
}
180181
}
@@ -186,8 +187,9 @@ export class ReplayableEventEmitter<EventTypes extends EventMap> extends EventEm
186187
once<K extends keyof EventTypes>(event: K, listener: (...args: EventTypes[K]) => void): this;
187188
once(event: string | symbol, listener: (...args: any[]) => void): this {
188189
const eventName = event as keyof EventTypes;
189-
if (this.eventLog[eventName]) {
190-
for (const args of this.eventLog[eventName]!) {
190+
const eventLog = this.eventLog[eventName];
191+
if (eventLog) {
192+
for (const args of eventLog) {
191193
listener(...args);
192194
}
193195
}

0 commit comments

Comments
 (0)