diff --git a/components/dashboard/src/components/PrebuildLogs.tsx b/components/dashboard/src/components/PrebuildLogs.tsx index 408280ec981548..97bd20ddab2ab6 100644 --- a/components/dashboard/src/components/PrebuildLogs.tsx +++ b/components/dashboard/src/components/PrebuildLogs.tsx @@ -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, @@ -37,7 +37,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) { | undefined >(); const [error, setError] = useState(); - const [logsEmitter] = useState(new EventEmitter()); + const logsEmitter = useMemo(() => new EventEmitter(), []); const [prebuild, setPrebuild] = useState(); const handlePrebuildUpdate = useCallback( diff --git a/components/dashboard/src/components/WorkspaceLogs.tsx b/components/dashboard/src/components/WorkspaceLogs.tsx index ad7807ec1a0780..5d0ccd172a81b6 100644 --- a/components/dashboard/src/components/WorkspaceLogs.tsx +++ b/components/dashboard/src/components/WorkspaceLogs.tsx @@ -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(null); const terminalRef = useRef(); const fitAddon = useMemo(() => new FitAddon(), []); @@ -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(); @@ -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( () => { @@ -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) { @@ -112,12 +134,12 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) { return (
diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx index df2bd08c91cf53..2311ae718bc409 100644 --- a/components/dashboard/src/start/StartWorkspace.tsx +++ b/components/dashboard/src/start/StartWorkspace.tsx @@ -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"; @@ -760,7 +760,7 @@ interface ImageBuildViewProps { } function ImageBuildView(props: ImageBuildViewProps) { - const [logsEmitter] = useState(new EventEmitter()); + const logsEmitter = useMemo(() => new EventEmitter(), []); useEffect(() => { let registered = false; diff --git a/components/dashboard/src/utils.ts b/components/dashboard/src/utils.ts index 426c515200f9f3..489dbc6fd16a1d 100644 --- a/components/dashboard/src/utils.ts +++ b/components/dashboard/src/utils.ts @@ -173,8 +173,9 @@ export class ReplayableEventEmitter extends EventEm on(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); } } @@ -186,8 +187,9 @@ export class ReplayableEventEmitter extends EventEm once(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); } }