Skip to content

Commit

Permalink
Buffer logs to not overflow xterm (#20065)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
filiptronicek authored Jul 26, 2024
1 parent 21bb9ad commit ce6fd4d
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 20 deletions.
4 changes: 2 additions & 2 deletions components/dashboard/src/components/PrebuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import EventEmitter from "events";
import React, { Suspense, useCallback, useEffect, useState } from "react";
import React, { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import {
DisposableCollection,
WorkspaceImageBuild,
Expand Down Expand Up @@ -37,7 +37,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
| undefined
>();
const [error, setError] = useState<Error | undefined>();
const [logsEmitter] = useState(new EventEmitter());
const logsEmitter = useMemo(() => new EventEmitter(), []);
const [prebuild, setPrebuild] = useState<Prebuild | undefined>();

const handlePrebuildUpdate = useCallback(
Expand Down
46 changes: 34 additions & 12 deletions components/dashboard/src/components/WorkspaceLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ const lightTheme: ITheme = {
selectionBackground: "#add6ff80", // https://github.com/gitpod-io/gitpod-vscode-theme/blob/6fb17ba8915fcd68fde3055b4bc60642ce5eed14/themes/gitpod-light-color-theme.json#L15
};

export interface WorkspaceLogsProps {
export interface Props {
logsEmitter: EventEmitter;
errorMessage?: string;
classes?: string;
xtermClasses?: string;
}

export default function WorkspaceLogs(props: WorkspaceLogsProps) {
const MAX_CHUNK_SIZE = 1024 * 4; // 4KB

export default function WorkspaceLogs({ logsEmitter, errorMessage, classes, xtermClasses }: Props) {
const xTermParentRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal>();
const fitAddon = useMemo(() => new FitAddon(), []);
Expand All @@ -54,17 +56,37 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
terminal.loadAddon(fitAddon);
terminal.open(xTermParentRef.current);

const logListener = (logs: string) => {
if (terminal && logs) {
terminal.write(logs);
let logBuffer = "";
let isWriting = false;

const processNextLog = () => {
if (isWriting || logBuffer.length === 0) return;

const logs = logBuffer.slice(0, MAX_CHUNK_SIZE);
logBuffer = logBuffer.slice(logs.length);
if (logs) {
isWriting = true;
terminal.write(logs, () => {
isWriting = false;
processNextLog();
});
}
};

const logListener = (logs: string) => {
if (!logs) return;

logBuffer += logs;
processNextLog();
};

const resetListener = () => {
terminal.clear();
logBuffer = "";
isWriting = false;
};

const emitter = props.logsEmitter.on("logs", logListener);
const emitter = logsEmitter.on("logs", logListener);
emitter.on("reset", resetListener);
fitAddon.fit();

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

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

useEffect(() => {
if (terminalRef.current && props.errorMessage) {
terminalRef.current.write(`\r\n\u001b[38;5;196m${props.errorMessage}\u001b[0m\r\n`);
if (terminalRef.current && errorMessage) {
terminalRef.current.write(`\r\n\u001b[38;5;196m${errorMessage}\u001b[0m\r\n`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [terminalRef.current, props.errorMessage]);
}, [terminalRef.current, errorMessage]);

useEffect(() => {
if (!terminalRef.current) {
Expand All @@ -112,12 +134,12 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) {
return (
<div
className={cn(
props.classes || "mt-6 h-72 w-11/12 lg:w-3/5 rounded-xl overflow-hidden",
classes || "mt-6 h-72 w-11/12 lg:w-3/5 rounded-xl overflow-hidden",
"bg-pk-surface-secondary relative text-left",
)}
>
<div
className={cn(props.xtermClasses || "absolute top-0 left-0 bottom-0 right-0 m-6")}
className={cn(xtermClasses || "absolute top-0 left-0 bottom-0 right-0 m-6")}
ref={xTermParentRef}
></div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions components/dashboard/src/start/StartWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import EventEmitter from "events";
import * as queryString from "query-string";
import React, { Suspense, useEffect, useState } from "react";
import React, { Suspense, useEffect, useMemo } from "react";
import { v4 } from "uuid";
import Arrow from "../components/Arrow";
import ContextMenu from "../components/ContextMenu";
Expand Down Expand Up @@ -760,7 +760,7 @@ interface ImageBuildViewProps {
}

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

useEffect(() => {
let registered = false;
Expand Down
10 changes: 6 additions & 4 deletions components/dashboard/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ export class ReplayableEventEmitter<EventTypes extends EventMap> extends EventEm
on<K extends keyof EventTypes>(event: K, listener: (...args: EventTypes[K]) => void): this;
on(event: string | symbol, listener: (...args: any[]) => void): this {
const eventName = event as keyof EventTypes;
if (this.eventLog[eventName]) {
for (const args of this.eventLog[eventName]!) {
const eventLog = this.eventLog[eventName];
if (eventLog) {
for (const args of eventLog) {
listener(...args);
}
}
Expand All @@ -186,8 +187,9 @@ export class ReplayableEventEmitter<EventTypes extends EventMap> extends EventEm
once<K extends keyof EventTypes>(event: K, listener: (...args: EventTypes[K]) => void): this;
once(event: string | symbol, listener: (...args: any[]) => void): this {
const eventName = event as keyof EventTypes;
if (this.eventLog[eventName]) {
for (const args of this.eventLog[eventName]!) {
const eventLog = this.eventLog[eventName];
if (eventLog) {
for (const args of eventLog) {
listener(...args);
}
}
Expand Down

0 comments on commit ce6fd4d

Please sign in to comment.