Skip to content

Commit

Permalink
feat(dashboard): show current epoch and color for fragments & relatio…
Browse files Browse the repository at this point in the history
…ns (#19829)
  • Loading branch information
fuyufjh authored Dec 23, 2024
1 parent 26805d9 commit 3831db7
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 74 deletions.
118 changes: 99 additions & 19 deletions dashboard/components/FragmentGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ import {
layoutItem,
} from "../lib/layout"
import { PlanNodeDatum } from "../pages/fragment_graph"
import { FragmentStats } from "../proto/gen/monitor_service"
import { StreamNode } from "../proto/gen/stream_plan"
import { backPressureColor, backPressureWidth } from "./utils/backPressure"
import {
backPressureColor,
backPressureWidth,
epochToUnixMillis,
latencyToColor,
} from "./utils/backPressure"

const ReactJson = loadable(() => import("react-json-view"))

Expand Down Expand Up @@ -95,11 +101,13 @@ export default function FragmentGraph({
fragmentDependency,
selectedFragmentId,
backPressures,
fragmentStats,
}: {
planNodeDependencies: Map<string, d3.HierarchyNode<PlanNodeDatum>>
fragmentDependency: FragmentBox[]
selectedFragmentId?: string
backPressures?: Map<string, number>
fragmentStats?: { [fragmentId: number]: FragmentStats }
}) {
const svgRef = useRef<SVGSVGElement>(null)

Expand Down Expand Up @@ -188,6 +196,7 @@ export default function FragmentGraph({

useEffect(() => {
if (fragmentLayout) {
const now_ms = Date.now()
const svgNode = svgRef.current
const svgSelection = d3.select(svgNode)

Expand Down Expand Up @@ -246,13 +255,69 @@ export default function FragmentGraph({
.attr("height", ({ height }) => height - fragmentMarginY * 2)
.attr("x", fragmentMarginX)
.attr("y", fragmentMarginY)
.attr("fill", "white")
.attr(
"fill",
fragmentStats
? ({ id }) => {
const fragmentId = parseInt(id)
if (isNaN(fragmentId) || !fragmentStats[fragmentId]) {
return "white"
}
let currentMs = epochToUnixMillis(
fragmentStats[fragmentId].currentEpoch
)
return latencyToColor(now_ms - currentMs, "white")
}
: "white"
)
.attr("stroke-width", ({ id }) => (isSelected(id) ? 3 : 1))
.attr("rx", 5)
.attr("stroke", ({ id }) =>
isSelected(id) ? theme.colors.blue[500] : theme.colors.gray[500]
)

const getTooltipContent = (id: string) => {
const fragmentId = parseInt(id)
const stats = fragmentStats?.[fragmentId]
const latencySeconds = stats
? ((now_ms - epochToUnixMillis(stats.currentEpoch)) / 1000).toFixed(
2
)
: "N/A"
const epoch = stats?.currentEpoch ?? "N/A"

return `<b>Fragment ${fragmentId}</b><br>Epoch: ${epoch}<br>Latency: ${latencySeconds} seconds`
}

boundingBox
.on("mouseover", (event, { id }) => {
// Remove existing tooltip if any
d3.selectAll(".tooltip").remove()

// Create new tooltip
d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "white")
.style("padding", "10px")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
.style("font-size", "12px")
.html(getTooltipContent(id))
})
.on("mousemove", (event) => {
d3.select(".tooltip")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
})
.on("mouseout", () => {
d3.selectAll(".tooltip").remove()
})

// Stream node edges
let edgeSelection = gSel.select<SVGGElement>(".edges")
if (edgeSelection.empty()) {
Expand Down Expand Up @@ -409,24 +474,39 @@ export default function FragmentGraph({
.attr("stroke-width", width)
.attr("stroke", color)

// Tooltip for back pressure rate
let title = gSel.select<SVGTitleElement>("title")
if (title.empty()) {
title = gSel.append<SVGTitleElement>("title")
}

const text = (d: Edge) => {
if (backPressures) {
let value = backPressures.get(`${d.target}_${d.source}`)
if (value) {
return `${value.toFixed(2)}%`
path
.on("mouseover", (event, d) => {
// Remove existing tooltip if any
d3.selectAll(".tooltip").remove()

if (backPressures) {
const value = backPressures.get(`${d.target}_${d.source}`)
if (value) {
// Create new tooltip
d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "white")
.style("padding", "10px")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
.style("font-size", "12px")
.html(`BP: ${value.toFixed(2)}%`)
}
}
}

return ""
}

title.text(text)
})
.on("mousemove", (event) => {
d3.select(".tooltip")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
})
.on("mouseout", () => {
d3.selectAll(".tooltip").remove()
})

return gSel
}
Expand Down
132 changes: 104 additions & 28 deletions dashboard/components/RelationGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ import {
flipLayoutRelation,
generateRelationEdges,
} from "../lib/layout"
import { RelationStats } from "../proto/gen/monitor_service"
import { CatalogModal, useCatalogModal } from "./CatalogModal"
import { backPressureColor, backPressureWidth } from "./utils/backPressure"
import {
backPressureColor,
backPressureWidth,
epochToUnixMillis,
latencyToColor,
} from "./utils/backPressure"

function boundBox(
relationPosition: RelationPointPosition[],
Expand Down Expand Up @@ -62,11 +68,13 @@ export default function RelationGraph({
selectedId,
setSelectedId,
backPressures,
relationStats,
}: {
nodes: RelationPoint[]
selectedId: string | undefined
setSelectedId: (id: string) => void
backPressures?: Map<string, number> // relationId-relationId->back_pressure_rate})
relationStats: { [relationId: number]: RelationStats } | undefined
}) {
const [modalData, setModalId] = useCatalogModal(nodes.map((n) => n.relation))

Expand Down Expand Up @@ -99,6 +107,7 @@ export default function RelationGraph({
const { layoutMap, width, height, links } = layoutMapCallback()

useEffect(() => {
const now_ms = Date.now()
const svgNode = svgRef.current
const svgSelection = d3.select(svgNode)

Expand Down Expand Up @@ -150,24 +159,39 @@ export default function RelationGraph({
isSelected(d.source) || isSelected(d.target) ? 1 : 0.5
)

// Tooltip for back pressure rate
let title = sel.select<SVGTitleElement>("title")
if (title.empty()) {
title = sel.append<SVGTitleElement>("title")
}

const text = (d: Edge) => {
if (backPressures) {
let value = backPressures.get(`${d.target}_${d.source}`)
if (value) {
return `${value.toFixed(2)}%`
sel
.on("mouseover", (event, d) => {
// Remove existing tooltip if any
d3.selectAll(".tooltip").remove()

if (backPressures) {
const value = backPressures.get(`${d.target}_${d.source}`)
if (value) {
// Create new tooltip
d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "white")
.style("padding", "10px")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
.style("font-size", "12px")
.html(`BP: ${value.toFixed(2)}%`)
}
}
}

return ""
}

title.text(text)
})
.on("mousemove", (event) => {
d3.select(".tooltip")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
})
.on("mouseout", () => {
d3.selectAll(".tooltip").remove()
})

return sel
}
Expand All @@ -189,9 +213,19 @@ export default function RelationGraph({

circle.attr("r", nodeRadius).attr("fill", ({ id, relation }) => {
const weight = relationIsStreamingJob(relation) ? "500" : "400"
return isSelected(id)
const baseColor = isSelected(id)
? theme.colors.blue[weight]
: theme.colors.gray[weight]
if (relationStats) {
const relationId = parseInt(id)
if (!isNaN(relationId) && relationStats[relationId]) {
const currentMs = epochToUnixMillis(
relationStats[relationId].currentEpoch
)
return latencyToColor(now_ms - currentMs, baseColor)
}
}
return baseColor
})

// Relation name
Expand Down Expand Up @@ -233,16 +267,50 @@ export default function RelationGraph({
.attr("font-size", 16)
.attr("font-weight", "bold")

// Relation type tooltip
let typeTooltip = g.select<SVGTitleElement>("title")
if (typeTooltip.empty()) {
typeTooltip = g.append<SVGTitleElement>("title")
// Tooltip
const getTooltipContent = (relation: Relation, id: string) => {
const relationId = parseInt(id)
const stats = relationStats?.[relationId]
const latencySeconds = stats
? (
(Date.now() - epochToUnixMillis(stats.currentEpoch)) /
1000
).toFixed(2)
: "N/A"
const epoch = stats?.currentEpoch ?? "N/A"

return `<b>${relation.name} (${relationTypeTitleCase(
relation
)})</b><br>Epoch: ${epoch}<br>Latency: ${latencySeconds} seconds`
}

typeTooltip.text(
({ relation }) =>
`${relation.name} (${relationTypeTitleCase(relation)})`
)
g.on("mouseover", (event, { relation, id }) => {
// Remove existing tooltip if any
d3.selectAll(".tooltip").remove()

// Create new tooltip
d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("background", "white")
.style("padding", "10px")
.style("border", "1px solid #ddd")
.style("border-radius", "4px")
.style("pointer-events", "none")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
.style("font-size", "12px")
.html(getTooltipContent(relation, id))
})
.on("mousemove", (event) => {
d3.select(".tooltip")
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY + 10 + "px")
})
.on("mouseout", () => {
d3.selectAll(".tooltip").remove()
})

// Relation modal
g.style("cursor", "pointer").on("click", (_, { relation, id }) => {
Expand All @@ -265,7 +333,15 @@ export default function RelationGraph({
nodeSelection.enter().call(createNode)
nodeSelection.call(applyNode)
nodeSelection.exit().remove()
}, [layoutMap, links, selectedId, setModalId, setSelectedId, backPressures])
}, [
layoutMap,
links,
selectedId,
setModalId,
setSelectedId,
backPressures,
relationStats,
])

return (
<>
Expand Down
Loading

0 comments on commit 3831db7

Please sign in to comment.