Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 379f1f4

Browse files
committed
Display staartup logs in a virtual DOM for performance
1 parent 34fde1a commit 379f1f4

File tree

9 files changed

+223
-46
lines changed

9 files changed

+223
-46
lines changed

agent/agent.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -649,27 +649,46 @@ func (a *agent) runScript(ctx context.Context, lifecycle, script string) error {
649649
_ = fileWriter.Close()
650650
}()
651651

652+
// Create pipes for startup logs reader and writer
652653
startupLogsReader, startupLogsWriter := io.Pipe()
654+
655+
// Close the pipes when the function returns
653656
defer func() {
654657
_ = startupLogsReader.Close()
655658
_ = startupLogsWriter.Close()
656659
}()
660+
661+
// Create a multi-writer for startup logs and file writer
657662
writer := io.MultiWriter(startupLogsWriter, fileWriter)
658663

664+
// Initialize variables for log management
659665
queuedLogs := make([]agentsdk.StartupLog, 0)
660666
var flushLogsTimer *time.Timer
661667
var logMutex sync.Mutex
662668
var logsSending bool
669+
670+
// sendLogs function uploads the queued logs to the server
663671
sendLogs := func() {
672+
// Lock logMutex and check if logs are already being sent
664673
logMutex.Lock()
665674
if logsSending {
666675
logMutex.Unlock()
667676
return
668677
}
669-
logsSending = true
678+
if flushLogsTimer != nil {
679+
flushLogsTimer.Stop()
680+
}
681+
if len(queuedLogs) == 0 {
682+
logMutex.Unlock()
683+
return
684+
}
685+
// Move the current queued logs to logsToSend and clear the queue
670686
logsToSend := queuedLogs
687+
logsSending = true
671688
queuedLogs = make([]agentsdk.StartupLog, 0)
672689
logMutex.Unlock()
690+
691+
// Retry uploading logs until successful or a specific error occurs
673692
for r := retry.New(time.Second, 5*time.Second); r.Wait(ctx); {
674693
err := a.client.PatchStartupLogs(ctx, agentsdk.PatchStartupLogs{
675694
Logs: logsToSend,
@@ -686,25 +705,30 @@ func (a *agent) runScript(ctx context.Context, lifecycle, script string) error {
686705
}
687706
a.logger.Error(ctx, "upload startup logs", slog.Error(err), slog.F("to_send", logsToSend))
688707
}
708+
// Reset logsSending flag
689709
logMutex.Lock()
690710
logsSending = false
691711
logMutex.Unlock()
692712
}
713+
// queueLog function appends a log to the queue and triggers sendLogs if necessary
693714
queueLog := func(log agentsdk.StartupLog) {
694715
logMutex.Lock()
695716
defer logMutex.Unlock()
717+
718+
// Append log to the queue
696719
queuedLogs = append(queuedLogs, log)
697-
if len(queuedLogs) > 25 {
720+
721+
// If there are more than 100 logs, send them immediately
722+
if len(queuedLogs) > 100 {
698723
go sendLogs()
699724
return
700725
}
726+
// Reset or set the flushLogsTimer to trigger sendLogs after 100 milliseconds
701727
if flushLogsTimer != nil {
702728
flushLogsTimer.Reset(100 * time.Millisecond)
703729
return
704730
}
705-
flushLogsTimer = time.AfterFunc(100*time.Millisecond, func() {
706-
sendLogs()
707-
})
731+
flushLogsTimer = time.AfterFunc(100*time.Millisecond, sendLogs)
708732
}
709733
err = a.trackConnGoroutine(func() {
710734
scanner := bufio.NewScanner(startupLogsReader)

coderd/workspaceagents.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,12 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
238238
if !httpapi.Read(ctx, rw, r, &req) {
239239
return
240240
}
241-
241+
if len(req.Logs) == 0 {
242+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
243+
Message: "No logs provided.",
244+
})
245+
return
246+
}
242247
createdAt := make([]time.Time, 0)
243248
output := make([]string, 0)
244249
outputLength := 0
@@ -342,11 +347,11 @@ func (api *API) patchWorkspaceAgentStartupLogs(rw http.ResponseWriter, r *http.R
342347
func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Request) {
343348
// This mostly copies how provisioner job logs are streamed!
344349
var (
345-
ctx = r.Context()
346-
agent = httpmw.WorkspaceAgentParam(r)
347-
logger = api.Logger.With(slog.F("workspace_agent_id", agent.ID))
348-
follow = r.URL.Query().Has("follow")
349-
afterRaw = r.URL.Query().Get("after")
350+
ctx = r.Context()
351+
workspaceAgent = httpmw.WorkspaceAgentParam(r)
352+
logger = api.Logger.With(slog.F("workspace_agent_id", workspaceAgent.ID))
353+
follow = r.URL.Query().Has("follow")
354+
afterRaw = r.URL.Query().Get("after")
350355
)
351356

352357
var after int64
@@ -366,7 +371,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
366371
}
367372

368373
logs, err := api.Database.GetWorkspaceAgentStartupLogsAfter(ctx, database.GetWorkspaceAgentStartupLogsAfterParams{
369-
AgentID: agent.ID,
374+
AgentID: workspaceAgent.ID,
370375
CreatedAfter: after,
371376
})
372377
if errors.Is(err, sql.ErrNoRows) {
@@ -412,7 +417,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
412417
if err != nil {
413418
return
414419
}
415-
if agent.LifecycleState == database.WorkspaceAgentLifecycleStateReady {
420+
if workspaceAgent.LifecycleState == database.WorkspaceAgentLifecycleStateReady {
416421
// The startup script has finished running, so we can close the connection.
417422
return
418423
}
@@ -434,7 +439,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
434439
}
435440

436441
closeSubscribe, err := api.Pubsub.Subscribe(
437-
agentsdk.StartupLogsNotifyChannel(agent.ID),
442+
agentsdk.StartupLogsNotifyChannel(workspaceAgent.ID),
438443
func(ctx context.Context, message []byte) {
439444
if endOfLogs.Load() {
440445
return
@@ -448,7 +453,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
448453

449454
if jlMsg.CreatedAfter != 0 {
450455
logs, err := api.Database.GetWorkspaceAgentStartupLogsAfter(ctx, database.GetWorkspaceAgentStartupLogsAfterParams{
451-
AgentID: agent.ID,
456+
AgentID: workspaceAgent.ID,
452457
CreatedAfter: jlMsg.CreatedAfter,
453458
})
454459
if err != nil {
@@ -461,7 +466,7 @@ func (api *API) workspaceAgentStartupLogs(rw http.ResponseWriter, r *http.Reques
461466
if jlMsg.EndOfLogs {
462467
endOfLogs.Store(true)
463468
logs, err := api.Database.GetWorkspaceAgentStartupLogsAfter(ctx, database.GetWorkspaceAgentStartupLogsAfterParams{
464-
AgentID: agent.ID,
469+
AgentID: workspaceAgent.ID,
465470
CreatedAfter: lastSentLogID.Load(),
466471
})
467472
if err != nil {

site/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
"react-markdown": "8.0.3",
7474
"react-router-dom": "6.4.1",
7575
"react-syntax-highlighter": "15.5.0",
76+
"react-virtualized-auto-sizer": "^1.0.7",
77+
"react-window": "^1.8.8",
7678
"remark-gfm": "3.0.1",
7779
"rollup-plugin-visualizer": "5.9.0",
7880
"sourcemapped-stacktrace": "1.1.11",
@@ -102,6 +104,8 @@
102104
"@types/react-dom": "18.0.6",
103105
"@types/react-helmet": "6.1.5",
104106
"@types/react-syntax-highlighter": "15.5.5",
107+
"@types/react-virtualized-auto-sizer": "^1.0.1",
108+
"@types/react-window": "^1.8.5",
105109
"@types/semver": "7.3.12",
106110
"@types/ua-parser-js": "0.7.36",
107111
"@types/uuid": "8.3.4",

site/src/components/DeploymentBanner/DeploymentBannerView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
192192
<Tooltip title="A countdown until stats are fetched again. Click to refresh!">
193193
<Button
194194
className={`${styles.value} ${styles.refreshButton}`}
195-
title="Refresh"
196195
onClick={() => {
197196
if (fetchStats) {
198197
fetchStats()

site/src/components/Logs/Logs.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,33 @@ export const Logs: FC<React.PropsWithChildren<LogsProps>> = ({
5151
)
5252
}
5353

54+
export const logLineHeight = 20
55+
56+
export const LogLine: FC<{
57+
line: Line
58+
hideTimestamp?: boolean
59+
number?: number
60+
style?: React.CSSProperties
61+
}> = ({ line, hideTimestamp, number, style }) => {
62+
const styles = useStyles({
63+
lineNumbers: Boolean(number),
64+
})
65+
66+
return (
67+
<div className={combineClasses([styles.line, line.level])} style={style}>
68+
{!hideTimestamp && (
69+
<>
70+
<span className={styles.time}>
71+
{number ? number : dayjs(line.time).format(`HH:mm:ss.SSS`)}
72+
</span>
73+
<span className={styles.space}>&nbsp;&nbsp;&nbsp;&nbsp;</span>
74+
</>
75+
)}
76+
<span>{line.output}</span>
77+
</div>
78+
)
79+
}
80+
5481
const useStyles = makeStyles<
5582
Theme,
5683
{
@@ -59,19 +86,20 @@ const useStyles = makeStyles<
5986
>((theme) => ({
6087
root: {
6188
minHeight: 156,
62-
background: theme.palette.background.default,
63-
color: theme.palette.text.primary,
64-
fontFamily: MONOSPACE_FONT_FAMILY,
6589
fontSize: 13,
66-
wordBreak: "break-all",
6790
padding: theme.spacing(2, 0),
6891
borderRadius: theme.shape.borderRadius,
6992
overflowX: "auto",
93+
background: theme.palette.background.default,
7094
},
7195
scrollWrapper: {
7296
width: "fit-content",
7397
},
7498
line: {
99+
wordBreak: "break-all",
100+
color: theme.palette.text.primary,
101+
fontFamily: MONOSPACE_FONT_FAMILY,
102+
height: logLineHeight,
75103
// Whitespace is significant in terminal output for alignment
76104
whiteSpace: "pre-line",
77105
padding: theme.spacing(0, 3),

site/src/components/Resources/AgentRow.tsx

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,23 @@ import { Skeleton } from "@material-ui/lab"
88
import { useMachine } from "@xstate/react"
99
import { AppLinkSkeleton } from "components/AppLink/AppLinkSkeleton"
1010
import { Maybe } from "components/Conditionals/Maybe"
11-
import { Line, Logs } from "components/Logs/Logs"
11+
import { LogLine, logLineHeight } from "components/Logs/Logs"
1212
import { PortForwardButton } from "components/PortForwardButton/PortForwardButton"
1313
import { VSCodeDesktopButton } from "components/VSCodeDesktopButton/VSCodeDesktopButton"
14-
import { FC, useEffect, useMemo, useRef, useState } from "react"
14+
import {
15+
FC,
16+
useCallback,
17+
useEffect,
18+
useLayoutEffect,
19+
useMemo,
20+
useRef,
21+
useState
22+
} from "react"
1523
import { useTranslation } from "react-i18next"
1624
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
1725
import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism"
26+
import AutoSizer from "react-virtualized-auto-sizer"
27+
import { FixedSizeList as List, ListOnScrollProps } from "react-window"
1828
import { workspaceAgentLogsMachine } from "xServices/workspaceAgentLogs/workspaceAgentLogsXService"
1929
import { Workspace, WorkspaceAgent } from "../../api/typesGenerated"
2030
import { AppLink } from "../AppLink/AppLink"
@@ -70,24 +80,49 @@ export const AgentRow: FC<AgentRowProps> = ({
7080
sendLogsEvent("FETCH_STARTUP_LOGS")
7181
}
7282
}, [sendLogsEvent, showStartupLogs])
83+
const logListRef = useRef<List>(null)
84+
const logListDivRef = useRef<HTMLDivElement>(null)
7385
const startupLogs = useMemo(() => {
74-
const logs =
75-
logsMachine.context.startupLogs?.map(
76-
(log): Line => ({
77-
level: "info",
78-
output: log.output,
79-
time: log.created_at,
80-
}),
81-
) || []
86+
const allLogs = logsMachine.context.startupLogs || []
87+
88+
const logs = [...allLogs]
8289
if (agent.startup_logs_overflowed) {
8390
logs.push({
91+
id: -1,
8492
level: "error",
8593
output: "Startup logs exceeded the max size of 1MB!",
8694
time: new Date().toISOString(),
8795
})
8896
}
8997
return logs
9098
}, [logsMachine.context.startupLogs, agent.startup_logs_overflowed])
99+
const [bottomOfLogs, setBottomOfLogs] = useState(true)
100+
useLayoutEffect(() => {
101+
if (bottomOfLogs && logListRef.current) {
102+
logListRef.current.scrollToItem(startupLogs.length - 1, "end")
103+
}
104+
}, [showStartupLogs, startupLogs, logListRef, bottomOfLogs])
105+
const handleLogScroll = useCallback(
106+
(props: ListOnScrollProps) => {
107+
if (
108+
props.scrollOffset === 0 ||
109+
props.scrollUpdateWasRequested ||
110+
!logListDivRef.current
111+
) {
112+
return
113+
}
114+
// The parent holds the height of the list!
115+
const parent = logListDivRef.current.parentElement
116+
if (!parent) {
117+
return
118+
}
119+
const distanceFromBottom =
120+
logListDivRef.current.scrollHeight -
121+
(props.scrollOffset + parent.clientHeight)
122+
setBottomOfLogs(distanceFromBottom < logLineHeight)
123+
},
124+
[logListDivRef],
125+
)
91126

92127
return (
93128
<Stack
@@ -269,8 +304,30 @@ export const AgentRow: FC<AgentRowProps> = ({
269304
)}
270305
</Stack>
271306
</Stack>
307+
272308
{showStartupLogs && (
273-
<Logs className={styles.startupLogs} lineNumbers lines={startupLogs} />
309+
<AutoSizer disableHeight>
310+
{({ width }) => (
311+
<List
312+
ref={logListRef}
313+
innerRef={logListDivRef}
314+
height={256}
315+
itemCount={startupLogs.length}
316+
itemSize={logLineHeight}
317+
width={width}
318+
className={styles.startupLogs}
319+
onScroll={handleLogScroll}
320+
>
321+
{({ index, style }) => (
322+
<LogLine
323+
line={startupLogs[index]}
324+
number={index + 1}
325+
style={style}
326+
/>
327+
)}
328+
</List>
329+
)}
330+
</AutoSizer>
274331
)}
275332
</Stack>
276333
)
@@ -312,8 +369,7 @@ const useStyles = makeStyles((theme) => ({
312369

313370
startupLogs: {
314371
maxHeight: 256,
315-
display: "flex",
316-
flexDirection: "column-reverse",
372+
background: theme.palette.background.default,
317373
},
318374

319375
startupScriptPopover: {

site/src/components/Workspace/Workspace.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Avatar } from "components/Avatar/Avatar"
33
import { AgentRow } from "components/Resources/AgentRow"
44
import {
55
ActiveTransition,
6-
WorkspaceBuildProgress
6+
WorkspaceBuildProgress,
77
} from "components/WorkspaceBuildProgress/WorkspaceBuildProgress"
88
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
99
import { FC } from "react"
@@ -15,7 +15,7 @@ import { Margins } from "../Margins/Margins"
1515
import {
1616
PageHeader,
1717
PageHeaderSubtitle,
18-
PageHeaderTitle
18+
PageHeaderTitle,
1919
} from "../PageHeader/PageHeader"
2020
import { Resources } from "../Resources/Resources"
2121
import { Stack } from "../Stack/Stack"

0 commit comments

Comments
 (0)