Skip to content

Commit 030c8ae

Browse files
authored
Don't show queue time in the run timeline by default (#1909)
* WIP on hiding the queued time * Fix for position/sizes of spans * Fix for duration squashing * Tweaked some styles on the timeline * Fix for retry spans appearing in the wrong place * Added “Q” shortcut key * Fix for in progress span durations
1 parent 61232ab commit 030c8ae

File tree

3 files changed

+99
-27
lines changed

3 files changed

+99
-27
lines changed

apps/webapp/app/presenters/v3/RunPresenter.server.ts

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export class RunPresenter {
5050
spanId: true,
5151
friendlyId: true,
5252
status: true,
53+
startedAt: true,
5354
completedAt: true,
5455
logsDeletedAt: true,
5556
rootTaskRun: {
@@ -104,6 +105,7 @@ export class RunPresenter {
104105
spanId: run.spanId,
105106
status: run.status,
106107
isFinished: isFinalRunStatus(run.status),
108+
startedAt: run.startedAt,
107109
completedAt: run.completedAt,
108110
logsDeletedAt: showDeletedLogs ? null : run.logsDeletedAt,
109111
rootTaskRun: run.rootTaskRun,
@@ -201,6 +203,10 @@ export class RunPresenter {
201203
tree?.id === traceSummary.rootSpan.id ? undefined : traceSummary.rootSpan.runId,
202204
duration: totalDuration,
203205
rootStartedAt: tree?.data.startTime,
206+
startedAt: run.startedAt,
207+
queuedDuration: run.startedAt
208+
? millisecondsToNanoseconds(run.startedAt.getTime() - run.createdAt.getTime())
209+
: undefined,
204210
},
205211
};
206212
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx

+92-26
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade
295295
return <></>;
296296
}
297297

298-
const { events, parentRunFriendlyId, duration, rootSpanStatus, rootStartedAt } = trace;
298+
const { events, parentRunFriendlyId, duration, rootSpanStatus, rootStartedAt, queuedDuration } =
299+
trace;
299300
const shouldLiveReload = events.length <= maximumLiveReloadingSetting;
300301

301302
const changeToSpan = useDebounce((selectedSpan: string) => {
@@ -345,6 +346,7 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade
345346
totalDuration={duration}
346347
rootSpanStatus={rootSpanStatus}
347348
rootStartedAt={rootStartedAt ? new Date(rootStartedAt) : undefined}
349+
queuedDuration={queuedDuration}
348350
environmentType={run.environment.type}
349351
shouldLiveReload={shouldLiveReload}
350352
maximumLiveReloadingSetting={maximumLiveReloadingSetting}
@@ -472,6 +474,7 @@ type TasksTreeViewProps = {
472474
totalDuration: number;
473475
rootSpanStatus: "executing" | "completed" | "failed";
474476
rootStartedAt: Date | undefined;
477+
queuedDuration: number | undefined;
475478
environmentType: RuntimeEnvironmentType;
476479
shouldLiveReload: boolean;
477480
maximumLiveReloadingSetting: number;
@@ -491,6 +494,7 @@ function TasksTreeView({
491494
totalDuration,
492495
rootSpanStatus,
493496
rootStartedAt,
497+
queuedDuration,
494498
environmentType,
495499
shouldLiveReload,
496500
maximumLiveReloadingSetting,
@@ -502,12 +506,14 @@ function TasksTreeView({
502506
const [errorsOnly, setErrorsOnly] = useState(false);
503507
const [showDebug, setShowDebug] = useState(false);
504508
const [showDurations, setShowDurations] = useState(true);
509+
const [showQueueTime, setShowQueueTime] = useState(false);
505510
const [scale, setScale] = useState(0);
506511
const parentRef = useRef<HTMLDivElement>(null);
507512
const treeScrollRef = useRef<HTMLDivElement>(null);
508513
const timelineScrollRef = useRef<HTMLDivElement>(null);
509514

510515
const displayEvents = showDebug ? events : events.filter((event) => !event.data.isDebug);
516+
const queuedTime = showQueueTime ? undefined : queuedDuration;
511517

512518
const {
513519
nodes,
@@ -556,6 +562,13 @@ function TasksTreeView({
556562
onCheckedChange={(e) => setShowDebug(e.valueOf())}
557563
/>
558564
)}
565+
<Switch
566+
variant="small"
567+
label="Queue time"
568+
checked={showQueueTime}
569+
onCheckedChange={(e) => setShowQueueTime(e.valueOf())}
570+
shortcut={{ key: "Q" }}
571+
/>
559572
<Switch
560573
variant="small"
561574
label="Errors only"
@@ -692,6 +705,7 @@ function TasksTreeView({
692705
events={events}
693706
rootSpanStatus={rootSpanStatus}
694707
rootStartedAt={rootStartedAt}
708+
queuedDuration={queuedTime}
695709
parentRef={parentRef}
696710
timelineScrollRef={timelineScrollRef}
697711
nodes={nodes}
@@ -754,7 +768,7 @@ function TasksTreeView({
754768

755769
type TimelineViewProps = Pick<
756770
TasksTreeViewProps,
757-
"totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt"
771+
"totalDuration" | "rootSpanStatus" | "events" | "rootStartedAt" | "queuedDuration"
758772
> & {
759773
scale: number;
760774
parentRef: React.RefObject<HTMLDivElement>;
@@ -785,27 +799,32 @@ function TimelineView({
785799
toggleNodeSelection,
786800
showDurations,
787801
treeScrollRef,
802+
queuedDuration,
788803
}: TimelineViewProps) {
789-
const isAdmin = useHasAdminAccess();
790804
const timelineContainerRef = useRef<HTMLDivElement>(null);
791805
const initialTimelineDimensions = useInitialDimensions(timelineContainerRef);
792806
const minTimelineWidth = initialTimelineDimensions?.width ?? 300;
793807
const maxTimelineWidth = minTimelineWidth * 10;
794808

795809
//we want to live-update the duration if the root span is still executing
796-
const [duration, setDuration] = useState(totalDuration);
810+
const [duration, setDuration] = useState(queueAdjustedNs(totalDuration, queuedDuration));
797811
useEffect(() => {
798812
if (rootSpanStatus !== "executing" || !rootStartedAt) {
799-
setDuration(totalDuration);
813+
setDuration(queueAdjustedNs(totalDuration, queuedDuration));
800814
return;
801815
}
802816

803817
const interval = setInterval(() => {
804-
setDuration(millisecondsToNanoseconds(Date.now() - rootStartedAt.getTime()));
818+
setDuration(
819+
queueAdjustedNs(
820+
millisecondsToNanoseconds(Date.now() - rootStartedAt.getTime()),
821+
queuedDuration
822+
)
823+
);
805824
}, 500);
806825

807826
return () => clearInterval(interval);
808-
}, [totalDuration, rootSpanStatus]);
827+
}, [totalDuration, rootSpanStatus, queuedDuration, rootStartedAt]);
809828

810829
return (
811830
<div
@@ -820,7 +839,11 @@ function TimelineView({
820839
maxWidth={maxTimelineWidth}
821840
>
822841
{/* Follows the cursor */}
823-
<CurrentTimeIndicator totalDuration={duration} rootStartedAt={rootStartedAt} />
842+
<CurrentTimeIndicator
843+
totalDuration={duration}
844+
rootStartedAt={rootStartedAt}
845+
queuedDurationNs={queuedDuration}
846+
/>
824847

825848
<Timeline.Row className="grid h-full grid-rows-[2rem_1fr]">
826849
{/* The duration labels */}
@@ -920,6 +943,8 @@ function TimelineView({
920943
getTreeProps={getTreeProps}
921944
parentClassName="h-full scrollbar-hide"
922945
renderNode={({ node, state, index, virtualizer, virtualItem }) => {
946+
const isTopSpan = node.id === events[0]?.id;
947+
923948
return (
924949
<Timeline.Row
925950
key={index}
@@ -941,7 +966,9 @@ function TimelineView({
941966
eventIndex === 0 ? (
942967
<Timeline.Point
943968
key={eventIndex}
944-
ms={nanosecondsToMilliseconds(event.offset)}
969+
ms={nanosecondsToMilliseconds(
970+
queueAdjustedNs(event.offset, queuedDuration)
971+
)}
945972
>
946973
{(ms) => (
947974
<motion.div
@@ -956,13 +983,15 @@ function TimelineView({
956983
) : (
957984
<Timeline.Point
958985
key={eventIndex}
959-
ms={nanosecondsToMilliseconds(event.offset)}
986+
ms={nanosecondsToMilliseconds(
987+
queueAdjustedNs(event.offset, queuedDuration)
988+
)}
960989
className="z-10"
961990
>
962991
{(ms) => (
963992
<motion.div
964993
className={cn(
965-
"-ml-1 size-[0.3125rem] rounded-full border bg-background-bright",
994+
"-ml-[0.1562rem] size-[0.3125rem] rounded-full border bg-background-bright",
966995
eventBorderClassName(node.data)
967996
)}
968997
layoutId={`${node.id}-${event.name}`}
@@ -975,7 +1004,9 @@ function TimelineView({
9751004
node.data.timelineEvents[0] &&
9761005
node.data.timelineEvents[0].offset < node.data.offset ? (
9771006
<Timeline.Span
978-
startMs={nanosecondsToMilliseconds(node.data.timelineEvents[0].offset)}
1007+
startMs={nanosecondsToMilliseconds(
1008+
queueAdjustedNs(node.data.timelineEvents[0].offset, queuedDuration)
1009+
)}
9791010
durationMs={nanosecondsToMilliseconds(
9801011
node.data.offset - node.data.timelineEvents[0].offset
9811012
)}
@@ -988,21 +1019,35 @@ function TimelineView({
9881019
) : null}
9891020
<SpanWithDuration
9901021
showDuration={state.selected ? true : showDurations}
991-
startMs={nanosecondsToMilliseconds(node.data.offset)}
1022+
startMs={nanosecondsToMilliseconds(
1023+
Math.max(queueAdjustedNs(node.data.offset, queuedDuration), 0)
1024+
)}
9921025
durationMs={
9931026
node.data.duration
994-
? nanosecondsToMilliseconds(node.data.duration)
995-
: nanosecondsToMilliseconds(duration - node.data.offset)
1027+
? //completed
1028+
nanosecondsToMilliseconds(Math.min(node.data.duration, duration))
1029+
: //in progress
1030+
nanosecondsToMilliseconds(
1031+
Math.min(
1032+
duration + (queuedDuration ?? 0) - node.data.offset,
1033+
duration
1034+
)
1035+
)
9961036
}
9971037
node={node}
1038+
fadeLeft={isTopSpan && queuedDuration !== undefined}
9981039
/>
9991040
</>
10001041
) : (
1001-
<Timeline.Point ms={nanosecondsToMilliseconds(node.data.offset)}>
1042+
<Timeline.Point
1043+
ms={nanosecondsToMilliseconds(
1044+
queueAdjustedNs(node.data.offset, queuedDuration)
1045+
)}
1046+
>
10021047
{(ms) => (
10031048
<motion.div
10041049
className={cn(
1005-
"-ml-1 size-3 rounded-full border-2 border-background-bright",
1050+
"-ml-0.5 size-3 rounded-full border-2 border-background-bright",
10061051
eventBackgroundClassName(node.data)
10071052
)}
10081053
layoutId={node.id}
@@ -1027,6 +1072,14 @@ function TimelineView({
10271072
);
10281073
}
10291074

1075+
function queueAdjustedNs(timeNs: number, queuedDurationNs: number | undefined) {
1076+
if (queuedDurationNs) {
1077+
return timeNs - queuedDurationNs;
1078+
}
1079+
1080+
return timeNs;
1081+
}
1082+
10301083
function NodeText({ node }: { node: TraceEvent }) {
10311084
const className = "truncate";
10321085
return (
@@ -1180,15 +1233,18 @@ function PulsingDot() {
11801233
function SpanWithDuration({
11811234
showDuration,
11821235
node,
1236+
fadeLeft,
11831237
...props
1184-
}: Timeline.SpanProps & { node: TraceEvent; showDuration: boolean }) {
1238+
}: Timeline.SpanProps & { node: TraceEvent; showDuration: boolean; fadeLeft: boolean }) {
11851239
return (
11861240
<Timeline.Span {...props}>
11871241
<motion.div
11881242
className={cn(
1189-
"relative flex h-4 w-full min-w-0.5 items-center rounded-sm",
1190-
eventBackgroundClassName(node.data)
1243+
"relative flex h-4 w-full min-w-0.5 items-center",
1244+
eventBackgroundClassName(node.data),
1245+
fadeLeft ? "rounded-r-sm bg-gradient-to-r from-black/50 to-transparent" : "rounded-sm"
11911246
)}
1247+
style={{ backgroundSize: "20px 100%", backgroundRepeat: "no-repeat" }}
11921248
layoutId={node.id}
11931249
>
11941250
{node.data.isPartial && (
@@ -1197,19 +1253,22 @@ function SpanWithDuration({
11971253
style={{ backgroundImage: `url(${tileBgPath})`, backgroundSize: "8px 8px" }}
11981254
/>
11991255
)}
1200-
<div
1256+
<motion.div
12011257
className={cn(
1202-
"sticky left-0 z-10 transition group-hover:opacity-100",
1258+
"sticky left-0 z-10 transition-opacity group-hover:opacity-100",
12031259
!showDuration && "opacity-0"
12041260
)}
12051261
>
1206-
<div className="whitespace-nowrap rounded-sm px-1 py-0.5 text-xxs text-text-bright text-shadow-custom">
1262+
<motion.div
1263+
className="whitespace-nowrap rounded-sm px-1 py-0.5 text-xxs text-text-bright text-shadow-custom"
1264+
layout="position"
1265+
>
12071266
{formatDurationMilliseconds(props.durationMs, {
12081267
style: "short",
12091268
maxDecimalPoints: props.durationMs < 1000 ? 0 : 1,
12101269
})}
1211-
</div>
1212-
</div>
1270+
</motion.div>
1271+
</motion.div>
12131272
</motion.div>
12141273
</Timeline.Span>
12151274
);
@@ -1220,9 +1279,11 @@ const edgeBoundary = 0.17;
12201279
function CurrentTimeIndicator({
12211280
totalDuration,
12221281
rootStartedAt,
1282+
queuedDurationNs,
12231283
}: {
12241284
totalDuration: number;
12251285
rootStartedAt: Date | undefined;
1286+
queuedDurationNs: number | undefined;
12261287
}) {
12271288
return (
12281289
<Timeline.FollowCursor>
@@ -1235,7 +1296,11 @@ function CurrentTimeIndicator({
12351296
offset = lerp(0.5, 1, (ratio - (1 - edgeBoundary)) / edgeBoundary);
12361297
}
12371298

1238-
const currentTime = rootStartedAt ? new Date(rootStartedAt.getTime() + ms) : undefined;
1299+
const currentTime = rootStartedAt
1300+
? new Date(
1301+
rootStartedAt.getTime() + ms + nanosecondsToMilliseconds(queuedDurationNs ?? 0)
1302+
)
1303+
: undefined;
12391304
const currentTimeComponent = currentTime ? <DateTimeShort date={currentTime} /> : <></>;
12401305

12411306
return (
@@ -1300,6 +1365,7 @@ function KeyboardShortcuts({
13001365
title="Collapse all"
13011366
/>
13021367
<NumberShortcuts toggleLevel={(number) => toggleExpandLevel(number)} />
1368+
<ShortcutWithAction shortcut={{ key: "Q" }} title="Queue time" action={() => {}} />
13031369
</>
13041370
);
13051371
}

apps/webapp/app/v3/runEngineHandlers.server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ export function registerRunEngineEventBusHandlers() {
333333
}
334334

335335
await eventRepository.recordEvent(retryMessage, {
336+
startTime: BigInt(time.getTime() * 1000000),
336337
taskSlug: run.taskIdentifier,
337338
environment,
338339
attributes: {
@@ -347,7 +348,6 @@ export function registerRunEngineEventBusHandlers() {
347348
queueName: run.queue,
348349
},
349350
context: run.traceContext as Record<string, string | undefined>,
350-
spanIdSeed: `retry-${run.attemptNumber + 1}`,
351351
endTime: retryAt,
352352
});
353353
} catch (error) {

0 commit comments

Comments
 (0)