Home charts execution time per node type on a log scale, from a /observability/node-types rollup
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
|
||||
import type { NodeTypeTiming } from "@/client"
|
||||
import type { Range } from "@/components/Common/RangePicker"
|
||||
import { nodeTypesQueryOptions } from "@/components/Flow/queries"
|
||||
import { PANEL_SECTION } from "@/components/Flow/SidePanel"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { dur, si } from "@/lib/utils"
|
||||
import { CARD, nodeTypeTimingsQueryOptions } from "./queries"
|
||||
|
||||
/**
|
||||
* One row's columns: the type, its bar, its average. A phone puts the bar on a
|
||||
* line of its own under the other two, where it has the card's whole width.
|
||||
*/
|
||||
const COLUMNS =
|
||||
"grid grid-cols-[minmax(0,1fr)_auto] gap-x-3 sm:grid-cols-[12rem_minmax(0,1fr)_5rem]"
|
||||
|
||||
/** The fastest the axis reaches down to, in ms: a microsecond. */
|
||||
const FLOOR = 0.001
|
||||
|
||||
/**
|
||||
* The axis, in whole decades of milliseconds: from below the fastest type to
|
||||
* above the slowest, two decades at least so a bar has room to differ.
|
||||
*/
|
||||
function decades(timings: NodeTypeTiming[]): number[] {
|
||||
const measured = timings.map((t) => t.avg_ms).filter((ms) => ms >= FLOOR)
|
||||
if (!measured.length) return []
|
||||
const low = Math.floor(Math.log10(Math.min(...measured)))
|
||||
const high = Math.max(Math.ceil(Math.log10(Math.max(...measured))), low + 2)
|
||||
return Array.from({ length: high - low + 1 }, (_, i) => 10 ** (low + i))
|
||||
}
|
||||
|
||||
/** Where a duration sits along the axis, as a percentage. */
|
||||
function at(ms: number, axis: number[]): number {
|
||||
const low = Math.log10(axis[0])
|
||||
const high = Math.log10(axis[axis.length - 1])
|
||||
return ((Math.log10(ms) - low) / (high - low)) * 100
|
||||
}
|
||||
|
||||
/**
|
||||
* How long each kind of node takes per run, on a log scale.
|
||||
*
|
||||
* One bar per node type at its average, slowest first, so a hotspot is the top
|
||||
* row. The scale is logarithmic because node types sit decades apart, a timer
|
||||
* at microseconds and a model at seconds, and a linear one shows only the
|
||||
* slowest. A type that ran without a measurable duration has no bar.
|
||||
*/
|
||||
export function NodeTypeTimes({ range }: { range: Range }) {
|
||||
const { data, isPending } = useQuery(nodeTypeTimingsQueryOptions(range))
|
||||
const { data: types } = useQuery(nodeTypesQueryOptions())
|
||||
const titles = new Map((types ?? []).map((info) => [info.type, info.title]))
|
||||
|
||||
const timings = data ?? []
|
||||
const axis = decades(timings)
|
||||
|
||||
return (
|
||||
<section className="grid gap-3" data-testid="node-type-times">
|
||||
<div className="flex flex-wrap items-baseline gap-x-3">
|
||||
<h2 className={PANEL_SECTION}>Execution time per node type</h2>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
average per run over the last {range.label}, log scale
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className={CARD}>
|
||||
{isPending ? (
|
||||
<div className="grid gap-3">
|
||||
<Skeleton className="h-5 w-2/3" />
|
||||
<Skeleton className="h-5 w-1/2" />
|
||||
</div>
|
||||
) : timings.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">Nothing has run yet.</p>
|
||||
) : (
|
||||
<div className="grid">
|
||||
{timings.map((timing) => (
|
||||
<Row
|
||||
key={timing.type}
|
||||
timing={timing}
|
||||
title={titles.get(timing.type) ?? timing.type}
|
||||
axis={axis}
|
||||
/>
|
||||
))}
|
||||
{axis.length ? (
|
||||
<div className={COLUMNS}>
|
||||
<div className="relative col-span-2 mt-1 h-4 text-muted-foreground text-xs sm:col-span-1 sm:col-start-2">
|
||||
{axis.map((ms, index) => (
|
||||
<span
|
||||
key={ms}
|
||||
className="absolute top-0 whitespace-nowrap"
|
||||
style={{
|
||||
left: `${at(ms, axis)}%`,
|
||||
// The ends stay inside the track; the rest centre on it.
|
||||
transform:
|
||||
index === 0
|
||||
? undefined
|
||||
: index === axis.length - 1
|
||||
? "translateX(-100%)"
|
||||
: "translateX(-50%)",
|
||||
}}
|
||||
>
|
||||
{dur(ms)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function Row({
|
||||
timing,
|
||||
title,
|
||||
axis,
|
||||
}: {
|
||||
timing: NodeTypeTiming
|
||||
title: string
|
||||
axis: number[]
|
||||
}) {
|
||||
const width =
|
||||
timing.avg_ms >= FLOOR && axis.length ? at(timing.avg_ms, axis) : 0
|
||||
return (
|
||||
<div className={COLUMNS}>
|
||||
<div className="col-start-1 row-start-1 min-w-0 pt-1.5 sm:py-1.5">
|
||||
<p className="truncate text-sm">{title}</p>
|
||||
<p className="truncate text-muted-foreground text-xs">
|
||||
{si(timing.executions)} runs · peak {dur(timing.max_ms)}
|
||||
</p>
|
||||
</div>
|
||||
{/* The decade lines run through every row, so together they read as
|
||||
the grid; the bar starts at the axis's own floor. */}
|
||||
<div className="relative col-span-2 row-start-2 h-4 sm:col-span-1 sm:col-start-2 sm:row-start-1 sm:h-full">
|
||||
{axis.map((ms) => (
|
||||
<span
|
||||
key={ms}
|
||||
className="absolute inset-y-0 w-px bg-border"
|
||||
style={{ left: `${at(ms, axis)}%` }}
|
||||
/>
|
||||
))}
|
||||
<span
|
||||
className="absolute top-1/2 left-0 h-2.5 -translate-y-1/2 rounded-e-[4px] bg-chart-1"
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="col-start-2 row-start-1 self-center whitespace-nowrap text-right font-mono text-sm sm:col-start-3">
|
||||
{dur(timing.avg_ms)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -147,3 +147,10 @@ export const clock = (ts: number) =>
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
|
||||
export const nodeTypeTimingsQueryOptions = (range: Range) => ({
|
||||
queryKey: ["observability", "node-types", range.hours] as const,
|
||||
queryFn: () =>
|
||||
ObservabilityService.readNodeTypeTimings({ hours: range.hours }),
|
||||
refetchInterval: REFRESH,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user