From 08badabecde0b403d965a9ffbd8eaa4ac68cff3b Mon Sep 17 00:00:00 2001 From: Pierre GIRAUD Date: Fri, 23 Feb 2024 12:23:44 +0100 Subject: [PATCH] Create DiagramRow component The idea is to split Diagram.vue into smaller components. Also we can use useNode to manage tooltips. --- src/components/Diagram.vue | 689 +------------------------------- src/components/DiagramRow.vue | 532 ++++++++++++++++++++++++ src/components/LevelDivider.vue | 32 ++ src/components/Plan.vue | 1 + src/interfaces.ts | 2 + src/node.ts | 273 ++++++++++++- src/services/plan-service.ts | 10 + 7 files changed, 866 insertions(+), 673 deletions(-) create mode 100644 src/components/DiagramRow.vue create mode 100644 src/components/LevelDivider.vue diff --git a/src/components/Diagram.vue b/src/components/Diagram.vue index ed243e19..3e721f41 100644 --- a/src/components/Diagram.vue +++ b/src/components/Diagram.vue @@ -6,22 +6,18 @@ import { nextTick, onBeforeMount, onMounted, + provide, reactive, ref, watch, } from "vue" import type { Ref } from "vue" import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome" -import { blocks, duration, rows, factor, transferRate } from "@/filters" -import { EstimateDirection, BufferLocation, NodeProp, Metric } from "../enums" +import { BufferLocation, NodeProp, Metric } from "../enums" import { HelpService, scrollChildIntoParentView } from "@/services/help-service" import type { IPlan, Node } from "@/interfaces" -import { - HighlightedNodeIdKey, - PlanKey, - SelectedNodeIdKey, - SelectNodeKey, -} from "@/symbols" +import { HighlightedNodeIdKey, PlanKey, SelectNodeKey } from "@/symbols" +import DiagramRow from "@/components/DiagramRow.vue" import { directive as vTippy } from "vue-tippy" import tippy, { createSingleton } from "tippy.js" @@ -36,15 +32,12 @@ const plan = inject(PlanKey) as Ref const container = ref(null) // The container element -const selectedNodeId = inject(SelectedNodeIdKey) const selectNode = inject(SelectNodeKey) if (!selectNode) { throw new Error(`Could not resolve ${SelectNodeKey.description}`) } const highlightedNodeId = inject(HighlightedNodeIdKey) -const rowRefs: Element[] = [] - // The main plan + init plans (all flatten) let plans: Row[][] = [[]] let tippyInstances: Instance[] = [] @@ -101,168 +94,6 @@ function loadTooltips(): void { }) } -function getTooltipContent(node: Node): string { - let content = "" - switch (viewOptions.metric) { - case Metric.time: - content += timeTooltip(node) - break - case Metric.rows: - content += rowsTooltip(node) - break - case Metric.estimate_factor: - content += estimateFactorTooltip(node) - break - case Metric.cost: - content += costTooltip(node) - break - case Metric.buffers: - content += buffersTooltip(node) - break - case Metric.io: - content += ioTooltip(node) - break - } - if (node[NodeProp.CTE_NAME]) { - content += "
CTE " + node[NodeProp.CTE_NAME] + "" - } - return content -} - -function timeTooltip(node: Node): string { - return [ - "Duration:
Exclusive: ", - duration(node[NodeProp.EXCLUSIVE_DURATION]), - ", Total: ", - duration(node[NodeProp.ACTUAL_TOTAL_TIME]), - ].join("") -} - -function rowsTooltip(node: Node): string { - return ["Rows: ", rows(node[NodeProp.ACTUAL_ROWS_REVISED] as number)].join("") -} - -function estimateFactorTooltip(node: Node): string { - const estimateFactor = node[NodeProp.PLANNER_ESTIMATE_FACTOR] - const estimateDirection = node[NodeProp.PLANNER_ESTIMATE_DIRECTION] - let text = "" - if (estimateFactor === undefined || estimateDirection === undefined) { - return "N/A" - } - switch (estimateDirection) { - case EstimateDirection.over: - text += ' over' - break - case EstimateDirection.under: - text += ' under' - break - default: - text += "Correctly" - } - text += " estimated" - text += - estimateFactor !== 1 ? " by " + factor(estimateFactor) + "" : "" - text += "
" - text += "Planned: " + node[NodeProp.PLAN_ROWS_REVISED] - text += " → Actual: " + node[NodeProp.ACTUAL_ROWS_REVISED] - return text -} - -function costTooltip(node: Node): string { - return ["Cost: ", rows(node[NodeProp.EXCLUSIVE_COST] as number)].join("") -} - -function buffersTooltip(node: Node): string { - let text = "" - let hit - let read - let written - let dirtied - switch (viewOptions.buffersMetric) { - case BufferLocation.shared: - hit = node[NodeProp.EXCLUSIVE_SHARED_HIT_BLOCKS] - read = node[NodeProp.EXCLUSIVE_SHARED_READ_BLOCKS] - dirtied = node[NodeProp.EXCLUSIVE_SHARED_DIRTIED_BLOCKS] - written = node[NodeProp.EXCLUSIVE_SHARED_WRITTEN_BLOCKS] - break - case BufferLocation.temp: - read = node[NodeProp.EXCLUSIVE_TEMP_READ_BLOCKS] - written = node[NodeProp.EXCLUSIVE_TEMP_WRITTEN_BLOCKS] - break - case BufferLocation.local: - hit = node[NodeProp.EXCLUSIVE_LOCAL_HIT_BLOCKS] - read = node[NodeProp.EXCLUSIVE_LOCAL_READ_BLOCKS] - dirtied = node[NodeProp.EXCLUSIVE_LOCAL_DIRTIED_BLOCKS] - written = node[NodeProp.EXCLUSIVE_LOCAL_WRITTEN_BLOCKS] - break - } - text += '' - text += hit - ? '" - : "" - text += read - ? '" - : "" - text += dirtied - ? '" - : "" - text += written - ? '" - : "" - text += "
Hit:' + blocks(hit) + "
Read:' + blocks(read) + "
Dirtied:' + - blocks(dirtied) + - "
Written:' + - blocks(written) + - "
" - - if (!hit && !read && !dirtied && !written) { - text = " N/A" - } - - switch (viewOptions.buffersMetric) { - case BufferLocation.shared: - text = "Shared Blocks:" + text - break - case BufferLocation.temp: - text = "Temp Blocks:" + text - break - case BufferLocation.local: - text = "Local Blocks:" + text - break - } - return text -} - -function ioTooltip(node: Node): string { - let text = "" - const read = node[NodeProp.EXCLUSIVE_IO_READ_TIME] - const averageRead = node[NodeProp.AVERAGE_IO_READ_TIME] - const write = node[NodeProp.EXCLUSIVE_IO_WRITE_TIME] - const averageWrite = node[NodeProp.AVERAGE_IO_WRITE_TIME] - text += '' - text += read - ? '" - : "" - text += write - ? '" - : "" - return "IO " + text -} - -function nodeType(row: Row): string { - return row[1][NodeProp.NODE_TYPE] -} - function flatten( output: Row[], level: number, @@ -290,33 +121,6 @@ function flatten( } } -function estimateFactorPercent(row: Row): number { - const node = row[1] - if (node[NodeProp.PLANNER_ESTIMATE_FACTOR] === Infinity) { - return 100 - } - return ( - ((node[NodeProp.PLANNER_ESTIMATE_FACTOR] || 0) / maxEstimateFactor.value) * - 100 - ) -} - -const maxEstimateFactor = computed((): number => { - const max = _.max( - _.map(plans, (plan) => { - return _.max( - _.map(plan, (row) => { - const f = row[1][NodeProp.PLANNER_ESTIMATE_FACTOR] - if (f !== Infinity) { - return f - } - }) - ) - }) - ) as number - return max * 2 || 1 -}) - const dataAvailable = computed((): boolean => { if (viewOptions.metric === Metric.buffers) { // if current Metric is buffers, view options for buffers should be @@ -330,19 +134,14 @@ function isCTE(node: Node): boolean { return _.startsWith(node[NodeProp.SUBPLAN_NAME], "CTE") } -watch( - () => selectedNodeId?.value, - (newVal) => { - if (!container.value || !newVal) { - return - } - scrollChildIntoParentView(container.value, rowRefs[newVal as number], false) +function scrollTo(el: Element) { + if (!container.value) { + return } -) - -function setRowRef(nodeId: number, el: Element) { - rowRefs[nodeId] = el + scrollChildIntoParentView(container.value, el, false) } + +provide("scrollTo", scrollTo)
Read:' + - duration(read) + - "
~" + - transferRate(averageRead) + - "" + - "
Write:' + - duration(write) + - "
~" + - transferRate(averageWrite) + - "" + - "
diff --git a/src/components/DiagramRow.vue b/src/components/DiagramRow.vue new file mode 100644 index 00000000..82907fa8 --- /dev/null +++ b/src/components/DiagramRow.vue @@ -0,0 +1,532 @@ + + + diff --git a/src/components/LevelDivider.vue b/src/components/LevelDivider.vue new file mode 100644 index 00000000..d8adaccf --- /dev/null +++ b/src/components/LevelDivider.vue @@ -0,0 +1,32 @@ + + diff --git a/src/components/Plan.vue b/src/components/Plan.vue index 805cc889..6c89ffeb 100644 --- a/src/components/Plan.vue +++ b/src/components/Plan.vue @@ -163,6 +163,7 @@ onBeforeMount(() => { planStats.maxDuration = content.maxDuration || NaN planStats.maxBlocks = content.maxBlocks || ({} as IBlocksStats) planStats.maxIo = content.maxIo || NaN + planStats.maxEstimateFactor = content.maxEstimateFactor || NaN planStats.triggers = content.Triggers || [] planStats.jitTime = (content.JIT && content.JIT.Timing && content.JIT.Timing.Total) || NaN diff --git a/src/interfaces.ts b/src/interfaces.ts index eea5697d..aabfea10 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -26,6 +26,7 @@ export interface IPlanContent { maxDuration?: number maxBlocks?: IBlocksStats maxIo?: number + maxEstimateFactor?: number Triggers?: ITrigger[] JIT?: JIT "Query Text"?: string @@ -54,6 +55,7 @@ export interface IPlanStats { maxDuration: number maxBlocks: IBlocksStats maxIo: number + maxEstimateFactor: number triggers?: ITrigger[] jitTime?: number settings?: Settings diff --git a/src/node.ts b/src/node.ts index eb7484dd..045b2880 100644 --- a/src/node.ts +++ b/src/node.ts @@ -3,8 +3,13 @@ import _ from "lodash" import { computed, onBeforeMount, ref, watch } from "vue" import type { Ref } from "vue" import type { IPlan, Node, Worker, ViewOptions } from "@/interfaces" -import { NodeProp, EstimateDirection, HighlightType } from "@/enums" -import { cost, duration, rows } from "@/filters" +import { + BufferLocation, + NodeProp, + EstimateDirection, + HighlightType, +} from "@/enums" +import { blocks, cost, duration, factor, rows, transferRate } from "@/filters" import { numberToColorHsl } from "@/services/color-service" export default function useNode( @@ -247,26 +252,288 @@ export default function useNode( return [...Array(workersPlanned).keys()].slice().reverse() }) + const estimateFactorPercent = computed((): number => { + switch (node[NodeProp.PLANNER_ESTIMATE_FACTOR]) { + case Infinity: + return 100 + case 1: + return 0 + default: + return ( + ((node[NodeProp.PLANNER_ESTIMATE_FACTOR] || 0) / + plan.value.planStats.maxEstimateFactor) * + 100 + ) + } + }) + + const sharedHitPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_SHARED_HIT_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.shared]) * + 100 + ) + }) + + const sharedReadPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_SHARED_READ_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.shared]) * + 100 + ) + }) + + const sharedDirtiedPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_SHARED_DIRTIED_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.shared]) * + 100 + ) + }) + + const sharedWrittenPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_SHARED_WRITTEN_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.shared]) * + 100 + ) + }) + + const tempReadPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_TEMP_READ_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.temp]) * + 100 + ) + }) + + const tempWrittenPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_TEMP_WRITTEN_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.temp]) * + 100 + ) + }) + + const localHitPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_LOCAL_HIT_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.local]) * + 100 + ) + }) + + const localReadPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_LOCAL_READ_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.local]) * + 100 + ) + }) + + const localDirtiedPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_LOCAL_DIRTIED_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.local]) * + 100 + ) + }) + + const localWrittenPercent = computed((): number => { + return ( + (node[NodeProp.EXCLUSIVE_LOCAL_WRITTEN_BLOCKS] / + plan.value.planStats.maxBlocks?.[BufferLocation.local]) * + 100 + ) + }) + + const timeTooltip = computed((): string => { + return [ + "Duration:
Exclusive: ", + duration(node[NodeProp.EXCLUSIVE_DURATION]), + ", Total: ", + duration(node[NodeProp.ACTUAL_TOTAL_TIME]), + ].join("") + }) + + const rowsTooltip = computed((): string => { + return ["Rows: ", rows(node[NodeProp.ACTUAL_ROWS_REVISED] as number)].join( + "" + ) + }) + + const estimateFactorTooltip = computed((): string => { + const estimateFactor = node[NodeProp.PLANNER_ESTIMATE_FACTOR] + const estimateDirection = node[NodeProp.PLANNER_ESTIMATE_DIRECTION] + let text = "" + if (estimateFactor === undefined || estimateDirection === undefined) { + return "N/A" + } + switch (estimateDirection) { + case EstimateDirection.over: + text += "Over" + break + case EstimateDirection.under: + text += "Under" + break + default: + text += "Correctly" + } + text += " estimated" + text += + estimateFactor !== 1 ? " by " + factor(estimateFactor) + "" : "" + text += "
" + text += "Planned: " + node[NodeProp.PLAN_ROWS_REVISED] + text += " → Actual: " + node[NodeProp.ACTUAL_ROWS_REVISED] + return text + }) + + const costTooltip = computed((): string => { + return ["Cost: ", rows(node[NodeProp.EXCLUSIVE_COST] as number)].join("") + }) + + const buffersByLocationTooltip = computed( + () => + (location: BufferLocation): string => { + let text = "" + let hit + let read + let written + let dirtied + switch (location) { + case BufferLocation.shared: + hit = node[NodeProp.EXCLUSIVE_SHARED_HIT_BLOCKS] + read = node[NodeProp.EXCLUSIVE_SHARED_READ_BLOCKS] + dirtied = node[NodeProp.EXCLUSIVE_SHARED_DIRTIED_BLOCKS] + written = node[NodeProp.EXCLUSIVE_SHARED_WRITTEN_BLOCKS] + break + case BufferLocation.temp: + read = node[NodeProp.EXCLUSIVE_TEMP_READ_BLOCKS] + written = node[NodeProp.EXCLUSIVE_TEMP_WRITTEN_BLOCKS] + break + case BufferLocation.local: + hit = node[NodeProp.EXCLUSIVE_LOCAL_HIT_BLOCKS] + read = node[NodeProp.EXCLUSIVE_LOCAL_READ_BLOCKS] + dirtied = node[NodeProp.EXCLUSIVE_LOCAL_DIRTIED_BLOCKS] + written = node[NodeProp.EXCLUSIVE_LOCAL_WRITTEN_BLOCKS] + break + } + text += + '' + text += hit + ? '" + : "" + text += read + ? '" + : "" + text += dirtied + ? '" + : "" + text += written + ? '" + : "" + text += "
Hit:' + + blocks(hit) + + "
Read:' + + blocks(read) + + "
Dirtied:' + + blocks(dirtied) + + "
Written:' + + blocks(written) + + "
" + + if (!hit && !read && !dirtied && !written) { + text = " N/A" + } + + switch (location) { + case BufferLocation.shared: + text = "Shared Blocks:" + text + break + case BufferLocation.temp: + text = "Temp Blocks:" + text + break + case BufferLocation.local: + text = "Local Blocks:" + text + break + } + return text + } + ) + + const buffersByMetricTooltip = computed(() => (metric: NodeProp): string => { + let text = '' + text += `` + } + return text + }) + + const ioTooltip = computed((): string => { + let text = "" + const read = node[NodeProp.EXCLUSIVE_IO_READ_TIME] + const averageRead = node[NodeProp.AVERAGE_IO_READ_TIME] + const write = node[NodeProp.EXCLUSIVE_IO_WRITE_TIME] + const averageWrite = node[NodeProp.AVERAGE_IO_WRITE_TIME] + text += '
${metric}:` + if (node[metric]) { + text += `${blocks(node[metric] as number)}
' + text += read + ? '" + : "" + text += write + ? '" + : "" + return "IO " + text + }) + return { barColor, barWidth, + buffersByLocationTooltip, + buffersByMetricTooltip, costClass, + costTooltip, durationClass, + estimateFactorPercent, + estimateFactorTooltip, estimationClass, executionTimePercent, filterTooltip, + ioTooltip, heapFetchesClass, highlightValue, isNeverExecuted, isParallelAware, + localDirtiedPercent, + localHitPercent, + localReadPercent, + localWrittenPercent, nodeName, plannerRowEstimateDirection, plannerRowEstimateValue, rowsRemoved, - rowsRemovedProp, rowsRemovedClass, rowsRemovedPercent, rowsRemovedPercentString, + rowsRemovedProp, + rowsTooltip, + sharedDirtiedPercent, + sharedHitPercent, + sharedReadPercent, + sharedWrittenPercent, + tempReadPercent, + tempWrittenPercent, + timeTooltip, workersLaunchedCount, workersPlannedCount, workersPlannedCountReversed, diff --git a/src/services/plan-service.ts b/src/services/plan-service.ts index a574c066..c806f3a8 100644 --- a/src/services/plan-service.ts +++ b/src/services/plan-service.ts @@ -198,6 +198,16 @@ export class PlanService { if (highestIo && sumIo(highestIo)) { plan.content.maxIo = sumIo(highestIo) } + + const highestEstimateFactor = _.max( + _.map(flat, (node) => { + const f = node[NodeProp.PLANNER_ESTIMATE_FACTOR] + if (f !== Infinity) { + return f + } + }) + ) as number + plan.content.maxEstimateFactor = highestEstimateFactor * 2 || 1 } // actual duration and actual cost are calculated by subtracting child values from the total
Read:' + + duration(read) + + "
~" + + transferRate(averageRead) + + "" + + "
Write:' + + duration(write) + + "
~" + + transferRate(averageWrite) + + "" + + "