Skip to content

Commit e030cc2

Browse files
fix(webapp): optimize reconciliation to O(1) and add trailing-edge throttle
1 parent 4b19a1f commit e030cc2

File tree

2 files changed

+90
-48
lines changed

2 files changed

+90
-48
lines changed

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

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,41 @@ export class RunPresenter {
208208

209209
//we need the start offset for each item, and the total duration of the entire tree
210210
const treeRootStartTimeMs = tree ? tree?.data.startTime.getTime() : 0;
211+
212+
const postgresRunDuration =
213+
runData.isFinished && run.completedAt
214+
? millisecondsToNanoseconds(
215+
run.completedAt.getTime() -
216+
(run.rootTaskRun?.createdAt ?? run.createdAt).getTime()
217+
)
218+
: 0;
219+
211220
let totalDuration = tree?.data.duration ?? 0;
212221
const events = tree
213-
? flattenTree(tree).map((n) => {
214-
const offset = millisecondsToNanoseconds(
215-
n.data.startTime.getTime() - treeRootStartTimeMs
216-
);
222+
? flattenTree(tree).map((n, index) => {
223+
const isRoot = index === 0;
224+
const offset = millisecondsToNanoseconds(n.data.startTime.getTime() - treeRootStartTimeMs);
225+
226+
let nIsPartial = n.data.isPartial;
227+
let nDuration = n.data.duration;
228+
let nIsError = n.data.isError;
229+
230+
// NOTE: Clickhouse trace ingestion is eventually consistent.
231+
// When a run is marked finished in Postgres, we reconcile the
232+
// root span to reflect completion even if telemetry is still partial.
233+
// This is a deliberate UI-layer tradeoff to prevent stale or "stuck"
234+
// run states in the dashboard.
235+
if (isRoot && runData.isFinished && nIsPartial) {
236+
nIsPartial = false;
237+
nDuration = Math.max(nDuration ?? 0, postgresRunDuration);
238+
nIsError = isFailedRunStatus(runData.status);
239+
}
240+
217241
//only let non-debug events extend the total duration
218242
if (!n.data.isDebug) {
219-
totalDuration = Math.max(totalDuration, offset + n.data.duration);
243+
totalDuration = Math.max(totalDuration, offset + (nIsPartial ? 0 : nDuration));
220244
}
245+
221246
return {
222247
...n,
223248
data: {
@@ -228,23 +253,24 @@ export class RunPresenter {
228253
treeRootStartTimeMs
229254
),
230255
//set partial nodes to null duration
231-
duration: n.data.isPartial ? null : n.data.duration,
256+
duration: nIsPartial ? null : nDuration,
257+
isPartial: nIsPartial,
258+
isError: nIsError,
232259
offset,
233-
isRoot: n.id === traceSummary.rootSpan.id,
260+
isRoot,
234261
},
235262
};
236263
})
237264
: [];
238265

266+
if (runData.isFinished) {
267+
totalDuration = Math.max(totalDuration, postgresRunDuration);
268+
}
269+
239270
//total duration should be a minimum of 1ms
240271
totalDuration = Math.max(totalDuration, millisecondsToNanoseconds(1));
241272

242-
const reconciled = reconcileTraceWithRunLifecycle(
243-
runData,
244-
traceSummary.rootSpan.id,
245-
events,
246-
totalDuration
247-
);
273+
const reconciled = reconcileTraceWithRunLifecycle(runData, traceSummary.rootSpan.id, events, totalDuration);
248274

249275
return {
250276
run: runData,
@@ -285,14 +311,17 @@ export function reconcileTraceWithRunLifecycle(
285311
totalDuration: number;
286312
rootSpanStatus: "executing" | "completed" | "failed";
287313
} {
288-
const rootEvent = events.find((e) => e.id === rootSpanId);
289-
const currentStatus: "executing" | "completed" | "failed" = rootEvent
290-
? rootEvent.data.isError
291-
? "failed"
292-
: !rootEvent.data.isPartial
293-
? "completed"
294-
: "executing"
295-
: "executing";
314+
const rootEvent = events[0];
315+
const isActualRoot = rootEvent?.id === rootSpanId;
316+
317+
const currentStatus: "executing" | "completed" | "failed" =
318+
isActualRoot && rootEvent
319+
? rootEvent.data.isError
320+
? "failed"
321+
: !rootEvent.data.isPartial
322+
? "completed"
323+
: "executing"
324+
: "executing";
296325

297326
if (!runData.isFinished) {
298327
return { events, totalDuration, rootSpanStatus: currentStatus };
@@ -307,23 +336,28 @@ export function reconcileTraceWithRunLifecycle(
307336

308337
const updatedTotalDuration = Math.max(totalDuration, postgresRunDuration);
309338

310-
const updatedEvents = events.map((e) => {
311-
if (e.id === rootSpanId && e.data.isPartial) {
312-
return {
313-
...e,
314-
data: {
315-
...e.data,
316-
isPartial: false,
317-
duration: Math.max(e.data.duration ?? 0, postgresRunDuration),
318-
isError: isFailedRunStatus(runData.status),
319-
},
320-
};
321-
}
322-
return e;
323-
});
339+
// We only need to potentially update the root event (the first one) if it matches our ID
340+
if (isActualRoot && rootEvent && rootEvent.data.isPartial) {
341+
const updatedEvents = [...events];
342+
updatedEvents[0] = {
343+
...rootEvent,
344+
data: {
345+
...rootEvent.data,
346+
isPartial: false,
347+
duration: Math.max(rootEvent.data.duration ?? 0, postgresRunDuration),
348+
isError: isFailedRunStatus(runData.status),
349+
},
350+
};
351+
352+
return {
353+
events: updatedEvents,
354+
totalDuration: updatedTotalDuration,
355+
rootSpanStatus: isFailedRunStatus(runData.status) ? "failed" : "completed",
356+
};
357+
}
324358

325359
return {
326-
events: updatedEvents,
360+
events,
327361
totalDuration: updatedTotalDuration,
328362
rootSpanStatus: isFailedRunStatus(runData.status) ? "failed" : "completed",
329363
};

apps/webapp/app/utils/throttle.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
//From: https://kettanaito.com/blog/debounce-vs-throttle
2-
3-
/** A very simple throttle. Will execute the function at the end of each period and discard any other calls during that period. */
1+
/** A throttle that fires the first call immediately and ensures the last call during the duration is also fired. */
42
export function throttle(
53
func: (...args: any[]) => void,
64
durationMs: number
75
): (...args: any[]) => void {
8-
let isPrimedToFire = false;
9-
10-
return (...args: any[]) => {
11-
if (!isPrimedToFire) {
12-
isPrimedToFire = true;
6+
let timeoutId: NodeJS.Timeout | null = null;
7+
let nextArgs: any[] | null = null;
138

14-
setTimeout(() => {
15-
func(...args);
16-
isPrimedToFire = false;
17-
}, durationMs);
9+
const wrapped = (...args: any[]) => {
10+
if (timeoutId) {
11+
nextArgs = args;
12+
return;
1813
}
14+
15+
func(...args);
16+
17+
timeoutId = setTimeout(() => {
18+
timeoutId = null;
19+
if (nextArgs) {
20+
const argsToUse = nextArgs;
21+
nextArgs = null;
22+
wrapped(...argsToUse);
23+
}
24+
}, durationMs);
1925
};
26+
27+
return wrapped;
2028
}

0 commit comments

Comments
 (0)