Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): show current epoch and color for fragments & relations #19829

Merged
merged 10 commits into from
Dec 23, 2024
Merged
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
Loading