From c9106182f22c389159e3d31e14ba523cec9a25b4 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Thu, 19 Oct 2023 10:17:28 -0500 Subject: [PATCH 1/3] Support both dag and time horizontal layouts --- src/factories/box.ts | 25 ++++++++--- src/factories/nodes.ts | 26 +++++------ src/models/layout.ts | 10 ++--- src/workers/runGraph.ts | 6 ++- src/workers/runGraph.worker.ts | 81 ++++++++++++++++++++++++++++++++-- 5 files changed, 112 insertions(+), 36 deletions(-) diff --git a/src/factories/box.ts b/src/factories/box.ts index 4f5498e4..b11fb323 100644 --- a/src/factories/box.ts +++ b/src/factories/box.ts @@ -1,8 +1,9 @@ import { differenceInMilliseconds, millisecondsInSecond } from 'date-fns' import { Graphics } from 'pixi.js' -import { DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts' +import { DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, DEFAULT_TIME_COLUMN_SIZE_PIXELS } from '@/consts' import { RunGraphNode } from '@/models/RunGraph' import { waitForConfig } from '@/objects/config' +import { layout } from '@/objects/layout' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export async function nodeBoxFactory() { @@ -12,21 +13,31 @@ export async function nodeBoxFactory() { async function render(node: RunGraphNode): Promise { const { background } = config.styles.node(node) - const right = node.start_time - const left = node.end_time ?? new Date() - const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond - const boxWidth = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS - const boxHeight = config.styles.nodeHeight - config.styles.nodeMargin * 2 + const width = getWidth(node) + const height = config.styles.nodeHeight - config.styles.nodeMargin * 2 box.clear() box.lineStyle(1, 0x0, 1, 2) box.beginFill(background) - box.drawRoundedRect(0, 0, boxWidth, boxHeight, 4) + box.drawRoundedRect(0, 0, width, height, 4) box.endFill() return await box } + function getWidth(node: RunGraphNode): number { + if (layout.horizontal === 'time') { + const right = node.start_time + const left = node.end_time ?? new Date() + const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond + const width = seconds * DEFAULT_TIME_COLUMN_SIZE_PIXELS + + return width + } + + return DEFAULT_LINEAR_COLUMN_SIZE_PIXELS + } + return { box, render, diff --git a/src/factories/nodes.ts b/src/factories/nodes.ts index b83d482e..94da56e4 100644 --- a/src/factories/nodes.ts +++ b/src/factories/nodes.ts @@ -2,10 +2,9 @@ import { Container } from 'pixi.js' import { DEFAULT_NODES_CONTAINER_NAME, DEFAULT_POLL_INTERVAL } from '@/consts' import { NodeContainerFactory, nodeContainerFactory } from '@/factories/node' import { offsetsFactory } from '@/factories/offsets' -import { HorizontalPositionSettings } from '@/factories/position' import { horizontalSettingsFactory } from '@/factories/settings' -import { NodeLayoutRequest, NodeLayoutResponse, Pixels } from '@/models/layout' -import { RunGraphNode, RunGraphNodes } from '@/models/RunGraph' +import { NodeLayoutResponse, NodeWidths, Pixels } from '@/models/layout' +import { RunGraphData, RunGraphNode } from '@/models/RunGraph' import { waitForConfig } from '@/objects/config' import { exhaustive } from '@/utilities/exhaustive' import { WorkerLayoutMessage, WorkerMessage, layoutWorkerFactory } from '@/workers/runGraph' @@ -20,7 +19,6 @@ export async function nodesContainerFactory(runId: string) { const config = await waitForConfig() const rows = await offsetsFactory() - let settings: HorizontalPositionSettings let layout: NodeLayoutResponse = new Map() container.name = DEFAULT_NODES_CONTAINER_NAME @@ -28,32 +26,28 @@ export async function nodesContainerFactory(runId: string) { async function render(): Promise { const data = await config.fetch(runId) - settings = horizontalSettingsFactory(data.start_time) - - await renderNodes(data.nodes) + await renderRun(data) if (!data.end_time) { setTimeout(() => render(), DEFAULT_POLL_INTERVAL) } } - async function renderNodes(nodes: RunGraphNodes): Promise { - const request: NodeLayoutRequest = new Map() + async function renderRun(data: RunGraphData): Promise { + const widths: NodeWidths = new Map() - for (const [nodeId, node] of nodes) { + for (const [nodeId, node] of data.nodes) { // eslint-disable-next-line no-await-in-loop const { width } = await renderNode(node) - request.set(nodeId, { - node, - width, - }) + widths.set(nodeId, width) } worker.postMessage({ type: 'layout', - nodes: request, - settings, + data, + widths, + settings: horizontalSettingsFactory(data.start_time), }) } diff --git a/src/models/layout.ts b/src/models/layout.ts index eaa80921..23137865 100644 --- a/src/models/layout.ts +++ b/src/models/layout.ts @@ -1,17 +1,13 @@ -import { RunGraphNode } from '@/models/RunGraph' - export type Pixels = { x: number, y: number } export type VerticalMode = 'waterfall' | 'nearest-parent' -export type HorizontalMode = 'time' | 'dat' +export type HorizontalMode = 'time' | 'dag' + export type LayoutMode = { horizontal: HorizontalMode, vertical: VerticalMode, } -export type NodeLayoutRequest = Map +export type NodeWidths = Map export type NodeLayoutResponse = Map { + data.nodes.forEach((node, nodeId) => { + const x = horizontal.get(nodeId) + + if (x === undefined) { + console.warn(`NodeId not found in horizontal layout: Skipping ${node.label}`) + return + } + layout.set(nodeId, { - x: scale(node.start_time), + x, y: y++, }) }) @@ -39,3 +48,67 @@ function handleLayoutMessage({ nodes, settings }: ClientLayoutMessage): void { }) } +type HorizontalLayout = Map + +function getHorizontalLayout(message: ClientLayoutMessage): HorizontalLayout { + if (message.settings.mode === 'dag') { + return getHorizontalDagLayout(message) + } + + return getHorizontalTimeLayout(message) +} + +function getHorizontalDagLayout({ data, settings }: ClientLayoutMessage): HorizontalLayout { + const levels = getLevels(data.root_node_ids, data.nodes) + const scale = horizontalScaleFactory(settings) + const layout: HorizontalLayout = new Map() + + data.nodes.forEach((node, nodeId) => { + layout.set(nodeId, scale(levels.get(nodeId)!)) + }) + + return layout +} + +function getHorizontalTimeLayout({ data, settings }: ClientLayoutMessage): HorizontalLayout { + const scale = horizontalScaleFactory(settings) + const layout: HorizontalLayout = new Map() + + data.nodes.forEach((node, nodeId) => { + layout.set(nodeId, scale(node.start_time)) + }) + + return layout +} + +// Map +type NodeLevels = Map + +function getLevels(nodeIds: string[], nodes: RunGraphNodes, levels: NodeLevels = new Map()): NodeLevels { + nodeIds.forEach(nodeId => { + if (levels.has(nodeId)) { + return + } + + const node = nodes.get(nodeId) + + if (!node) { + throw new Error('Node id not found in nodes') + } + + const parentLevels = node.parents.map(({ id }) => levels.get(id) ?? 0) + + // -1 so that maxParentLevel + 1 is always at least 0 + const maxParentLevel = Math.max(...parentLevels, -1) + + levels.set(nodeId, maxParentLevel + 1) + + const childNodeIds = node.children.map(({ id }) => id) + + if (childNodeIds.length) { + getLevels(childNodeIds, nodes, levels) + } + }) + + return levels +} \ No newline at end of file From 8e40bc9ece1ef954ecacdafdbb92ac081b9d271c Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Thu, 19 Oct 2023 12:09:59 -0500 Subject: [PATCH 2/3] Support both trace and dependency layouts --- src/components/RunGraph.vue | 2 + src/components/RunGraphSettings.vue | 100 ++++++++++++++++++++++++++++ src/factories/box.ts | 2 +- src/factories/nodes.ts | 26 ++++++-- src/factories/position.ts | 8 +-- src/factories/settings.ts | 2 +- src/models/layout.ts | 2 +- src/objects/layout.ts | 19 +++++- src/workers/runGraph.worker.ts | 6 +- 9 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 src/components/RunGraphSettings.vue diff --git a/src/components/RunGraph.vue b/src/components/RunGraph.vue index 47d6dc11..edc5106c 100644 --- a/src/components/RunGraph.vue +++ b/src/components/RunGraph.vue @@ -4,12 +4,14 @@
+
+ + \ No newline at end of file diff --git a/src/factories/box.ts b/src/factories/box.ts index b11fb323..223ab0a0 100644 --- a/src/factories/box.ts +++ b/src/factories/box.ts @@ -26,7 +26,7 @@ export async function nodeBoxFactory() { } function getWidth(node: RunGraphNode): number { - if (layout.horizontal === 'time') { + if (layout.horizontal === 'trace') { const right = node.start_time const left = node.end_time ?? new Date() const seconds = differenceInMilliseconds(left, right) / millisecondsInSecond diff --git a/src/factories/nodes.ts b/src/factories/nodes.ts index 94da56e4..4123216d 100644 --- a/src/factories/nodes.ts +++ b/src/factories/nodes.ts @@ -6,6 +6,7 @@ import { horizontalSettingsFactory } from '@/factories/settings' import { NodeLayoutResponse, NodeWidths, Pixels } from '@/models/layout' import { RunGraphData, RunGraphNode } from '@/models/RunGraph' import { waitForConfig } from '@/objects/config' +import { emitter } from '@/objects/events' import { exhaustive } from '@/utilities/exhaustive' import { WorkerLayoutMessage, WorkerMessage, layoutWorkerFactory } from '@/workers/runGraph' @@ -19,21 +20,38 @@ export async function nodesContainerFactory(runId: string) { const config = await waitForConfig() const rows = await offsetsFactory() + let data: RunGraphData | null = null let layout: NodeLayoutResponse = new Map() container.name = DEFAULT_NODES_CONTAINER_NAME + emitter.on('layoutUpdated', () => renderRun()) + async function render(): Promise { - const data = await config.fetch(runId) + if (data === null) { + await fetch() + } - await renderRun(data) + if (data === null) { + throw new Error('Data was null after fetch') + } + + await renderRun() if (!data.end_time) { - setTimeout(() => render(), DEFAULT_POLL_INTERVAL) + setTimeout(() => fetch(), DEFAULT_POLL_INTERVAL) } } - async function renderRun(data: RunGraphData): Promise { + async function fetch(): Promise { + data = await config.fetch(runId) + } + + async function renderRun(): Promise { + if (data === null) { + return + } + const widths: NodeWidths = new Map() for (const [nodeId, node] of data.nodes) { diff --git a/src/factories/position.ts b/src/factories/position.ts index 7ab545aa..273e811e 100644 --- a/src/factories/position.ts +++ b/src/factories/position.ts @@ -7,14 +7,14 @@ export type HorizontalPositionSettings = { startTime: Date, timeSpan: number, timeSpanPixels: number, - dagColumnSize: number, + dependencyColumnSize: number, } export type HorizontalScale = ReturnType // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function horizontalScaleFactory(settings: HorizontalPositionSettings) { - if (settings.mode === 'time') { + if (settings.mode === 'trace') { return getTimeScale(settings) } @@ -36,6 +36,6 @@ function getTimeScale({ startTime, timeSpan, timeSpanPixels }: HorizontalPositio } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function getLinearScale({ dagColumnSize }: HorizontalPositionSettings) { - return scaleLinear().domain([0, 1]).range([0, dagColumnSize]) +function getLinearScale({ dependencyColumnSize }: HorizontalPositionSettings) { + return scaleLinear().domain([0, 1]).range([0, dependencyColumnSize]) } \ No newline at end of file diff --git a/src/factories/settings.ts b/src/factories/settings.ts index 3b52dbef..f8fcbbbb 100644 --- a/src/factories/settings.ts +++ b/src/factories/settings.ts @@ -8,6 +8,6 @@ export function horizontalSettingsFactory(startTime: Date): HorizontalPositionSe startTime, timeSpan: DEFAULT_TIME_COLUMN_SPAN_SECONDS, timeSpanPixels: DEFAULT_TIME_COLUMN_SIZE_PIXELS, - dagColumnSize: DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, + dependencyColumnSize: DEFAULT_LINEAR_COLUMN_SIZE_PIXELS, } } \ No newline at end of file diff --git a/src/models/layout.ts b/src/models/layout.ts index 23137865..b6c71d61 100644 --- a/src/models/layout.ts +++ b/src/models/layout.ts @@ -1,6 +1,6 @@ export type Pixels = { x: number, y: number } export type VerticalMode = 'waterfall' | 'nearest-parent' -export type HorizontalMode = 'time' | 'dag' +export type HorizontalMode = 'trace' | 'dependency' export type LayoutMode = { horizontal: HorizontalMode, diff --git a/src/objects/layout.ts b/src/objects/layout.ts index 61fa2303..e86d0565 100644 --- a/src/objects/layout.ts +++ b/src/objects/layout.ts @@ -1,12 +1,17 @@ +import { reactive } from 'vue' import { HorizontalMode, LayoutMode, VerticalMode } from '@/models/layout' import { emitter } from '@/objects/events' -export const layout: LayoutMode = { - horizontal: 'time', +export const layout: LayoutMode = reactive({ + horizontal: 'trace', vertical: 'waterfall', -} +}) export function setLayoutMode({ horizontal, vertical }: LayoutMode): void { + if (layout.horizontal === horizontal && layout.vertical === vertical) { + return + } + layout.horizontal = horizontal layout.vertical = vertical @@ -14,12 +19,20 @@ export function setLayoutMode({ horizontal, vertical }: LayoutMode): void { } export function setHorizontalMode(mode: HorizontalMode): void { + if (layout.horizontal === mode) { + return + } + layout.horizontal = mode emitter.emit('layoutUpdated', layout) } export function setVerticalMode(mode: VerticalMode): void { + if (layout.vertical === mode) { + return + } + layout.vertical = mode emitter.emit('layoutUpdated', layout) diff --git a/src/workers/runGraph.worker.ts b/src/workers/runGraph.worker.ts index 1165a33f..64a8e51d 100644 --- a/src/workers/runGraph.worker.ts +++ b/src/workers/runGraph.worker.ts @@ -51,14 +51,14 @@ function handleLayoutMessage(message: ClientLayoutMessage): void { type HorizontalLayout = Map function getHorizontalLayout(message: ClientLayoutMessage): HorizontalLayout { - if (message.settings.mode === 'dag') { - return getHorizontalDagLayout(message) + if (message.settings.mode === 'dependency') { + return getHorizontalDependencyLayout(message) } return getHorizontalTimeLayout(message) } -function getHorizontalDagLayout({ data, settings }: ClientLayoutMessage): HorizontalLayout { +function getHorizontalDependencyLayout({ data, settings }: ClientLayoutMessage): HorizontalLayout { const levels = getLevels(data.root_node_ids, data.nodes) const scale = horizontalScaleFactory(settings) const layout: HorizontalLayout = new Map() From 06bb9ed0730f5c83f859aa9b27f7eb2322e27853 Mon Sep 17 00:00:00 2001 From: Craig Harshbarger Date: Thu, 19 Oct 2023 14:08:00 -0500 Subject: [PATCH 3/3] Make sure to clear the interval if fetch is called again --- src/factories/nodes.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/factories/nodes.ts b/src/factories/nodes.ts index 4123216d..bed56819 100644 --- a/src/factories/nodes.ts +++ b/src/factories/nodes.ts @@ -22,10 +22,11 @@ export async function nodesContainerFactory(runId: string) { let data: RunGraphData | null = null let layout: NodeLayoutResponse = new Map() + let interval: ReturnType | undefined = undefined container.name = DEFAULT_NODES_CONTAINER_NAME - emitter.on('layoutUpdated', () => renderRun()) + emitter.on('layoutUpdated', () => renderNodes()) async function render(): Promise { if (data === null) { @@ -36,18 +37,20 @@ export async function nodesContainerFactory(runId: string) { throw new Error('Data was null after fetch') } - await renderRun() - - if (!data.end_time) { - setTimeout(() => fetch(), DEFAULT_POLL_INTERVAL) - } + await renderNodes() } async function fetch(): Promise { + clearInterval(interval) + data = await config.fetch(runId) + + if (!data.end_time) { + interval = setTimeout(() => fetch(), DEFAULT_POLL_INTERVAL) + } } - async function renderRun(): Promise { + async function renderNodes(): Promise { if (data === null) { return }