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): add backpressure to the relation dependency graph #18280

Merged
merged 44 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5017fdd
add ddl_graph page and DdlGraph component
kwannoel Aug 29, 2024
fcc6d11
get rid of plan node dependencies
kwannoel Sep 16, 2024
792529c
remove selected fragment id
kwannoel Sep 16, 2024
fb3ee36
cleanup more unused code
kwannoel Sep 16, 2024
92381f7
add sample DS, backpressure rates
kwannoel Sep 16, 2024
a033de7
add generateDdlEdges
kwannoel Sep 16, 2024
f17a4e3
complete component refactor of ddl graph
kwannoel Sep 16, 2024
b5a6704
build relation dep edges
kwannoel Sep 16, 2024
a62b4e6
exclude non-streaming relations from ddl graph
kwannoel Sep 17, 2024
019fc31
add method to map relation to fragment vertices
kwannoel Sep 17, 2024
249b3b4
add fragment id to vertex map
kwannoel Sep 17, 2024
f7dfe8d
use relation backpressure
kwannoel Sep 17, 2024
76dbe8c
format
kwannoel Sep 18, 2024
c336fc5
rename fragment to ddl
kwannoel Sep 18, 2024
18954fa
display schema name
kwannoel Sep 18, 2024
acc52f0
remove more useless things
kwannoel Sep 18, 2024
3a8993a
support prom
kwannoel Sep 18, 2024
0e05839
use circle instead
kwannoel Sep 18, 2024
9cb51c2
format
kwannoel Sep 19, 2024
4c22305
fix warn
kwannoel Sep 19, 2024
da3f361
simplify
kwannoel Sep 19, 2024
83dd9bb
extract out magic numbers
kwannoel Sep 19, 2024
22cf701
cleanup
kwannoel Sep 19, 2024
9d2c6ce
add button to reset backpressures
kwannoel Sep 19, 2024
c46b324
add button to reset embedded bp
kwannoel Sep 19, 2024
c2c54b5
cleanup code
kwannoel Sep 19, 2024
64bf784
clean
kwannoel Sep 19, 2024
3723a0e
format
kwannoel Sep 19, 2024
28c61aa
fix
kwannoel Sep 19, 2024
bcdba73
fix naming
kwannoel Sep 19, 2024
c4f735b
rename page title
kwannoel Sep 19, 2024
da1752c
use camelCase
kwannoel Sep 19, 2024
e3bfa6a
Update proto/meta.proto
kwannoel Sep 19, 2024
5e6115d
Update src/meta/src/dashboard/mod.rs
kwannoel Sep 19, 2024
0785a66
fix
kwannoel Sep 19, 2024
446b79e
fmt
kwannoel Sep 19, 2024
abb2a72
use relation deps api
kwannoel Sep 19, 2024
a6c04ec
rename to relation graph
kwannoel Sep 19, 2024
9f52231
format
kwannoel Sep 19, 2024
2f67710
add bp to dependency graph
kwannoel Sep 19, 2024
a8e06bd
remove obsolete
kwannoel Sep 19, 2024
ccfdf8a
add bp labels
kwannoel Sep 19, 2024
b16a249
typecheck
kwannoel Sep 19, 2024
3624e3a
reuse
kwannoel Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,24 @@ Start a RisingWave cluster, create some tables and materialized views for testin
For example:

```bash
./risedev d
./risedev d full
kwannoel marked this conversation as resolved.
Show resolved Hide resolved
./risedev slt e2e_test/nexmark/create_sources.slt.part
./risedev psql -c 'CREATE TABLE dimension (v1 int);'
./risedev psql -c 'CREATE MATERIALIZED VIEW mv AS SELECT auction.* FROM dimension join auction on auction.id-auction.id = dimension.v1;'
./risedev psql -c 'INSERT INTO dimension select 0 from generate_series(1, 50);'
./risedev psql -c 'CREATE MATERIALIZED VIEW mv2 AS SELECT * FROM mv;'
./risedev psql -c 'CREATE MATERIALIZED VIEW mv3 AS SELECT count(*) FROM mv2;'

./risedev psql -c 'CREATE MATERIALIZED VIEW mv4 AS SELECT * FROM mv;'
./risedev psql -c 'CREATE MATERIALIZED VIEW mv5 AS SELECT count(*) FROM mv2;'
./risedev psql -c 'CREATE MATERIALIZED VIEW mv6 AS SELECT mv4.* FROM mv4 join mv2 using(id);'
./risedev psql -c 'CREATE MATERIALIZED VIEW mv7 AS SELECT max(id) FROM mv;'

./risedev psql -c 'CREATE MATERIALIZED VIEW mv8 AS SELECT mv.* FROM mv join mv6 using(id);'
./risedev psql -c 'CREATE SCHEMA s1;'
./risedev psql -c 'CREATE TABLE s1.t1 (v1 int);'
./risedev psql -c 'CREATE MATERIALIZED VIEW s1.mv1 AS SELECT s1.t1.* FROM s1.t1 join mv on s1.t1.v1 = mv.id;'

./risedev psql -c 'INSERT INTO dimension select 0 from generate_series(1, 20);'
kwannoel marked this conversation as resolved.
Show resolved Hide resolved
```

Install dependencies and start the development server.
Expand Down
268 changes: 268 additions & 0 deletions dashboard/components/DdlGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
import { theme } from "@chakra-ui/react"
import { tinycolor } from "@ctrl/tinycolor"
import * as d3 from "d3"
import { cloneDeep } from "lodash"
import { Fragment, useCallback, useEffect, useRef } from "react"
import {
DdlBox,
Edge,
Enter,
Position,
generateDdlEdges,
layoutItem,
} from "../lib/layout"

const nodeRadius = 12
const ddlLayoutX = nodeRadius * 8
const ddlLayoutY = nodeRadius * 8
const ddlNameX = nodeRadius * 4
const ddlNameY = nodeRadius * 7
const ddlMarginX = nodeRadius * 2
const ddlMarginY = nodeRadius * 2

export default function DdlGraph({
ddlDependency,
backPressures,
}: {
ddlDependency: DdlBox[] // Ddl adjacency list, metadata
backPressures?: Map<string, number> // relationId-relationId->back_pressure_rate
}) {
const svgRef = useRef<SVGSVGElement>(null)

const ddlDependencyDagCallback = useCallback(() => {
const ddlDependencyDag = cloneDeep(ddlDependency)

const layoutDdlResult = new Map<string, any>()
const includedDdlIds = new Set<string>()
for (const ddlBox of ddlDependencyDag) {
let ddlId = ddlBox.id
layoutDdlResult.set(ddlId, {
ddlName: ddlBox.ddlName,
schemaName: ddlBox.schemaName,
})
includedDdlIds.add(ddlId)
}

const ddlLayout = layoutItem(
ddlDependencyDag.map(({ width: _1, height: _2, id, ...data }) => {
return { width: ddlLayoutX, height: ddlLayoutY, id, ...data }
}),
ddlMarginX,
ddlMarginY
)

let svgWidth = 0
let svgHeight = 0
ddlLayout.forEach(({ x, y }) => {
svgHeight = Math.max(svgHeight, y + ddlLayoutX)
svgWidth = Math.max(svgWidth, x + ddlLayoutY)
})
const edges = generateDdlEdges(ddlLayout)

return {
layoutResult: ddlLayout,
svgWidth,
svgHeight,
edges,
}
}, [ddlDependency])

const {
svgWidth,
svgHeight,
edges: ddlEdgeLayout,
layoutResult: ddlLayout,
} = ddlDependencyDagCallback()

useEffect(() => {
if (ddlLayout) {
const svgNode = svgRef.current
const svgSelection = d3.select(svgNode)

// Ddls
const applyDdl = (gSel: DdlSelection) => {
gSel.attr("transform", ({ x, y }) => `translate(${x}, ${y})`)

// Render ddl name
{
let text = gSel.select<SVGTextElement>(".text-frag-id")
if (text.empty()) {
text = gSel.append("text").attr("class", "text-frag-id")
}

text
.attr("fill", "black")
.text(({ ddlName, schemaName }) => `${schemaName}.${ddlName}`)
.attr("font-family", "inherit")
.attr("text-anchor", "middle")
.attr("dx", ddlNameX)
.attr("dy", ddlNameY)
.attr("fill", "black")
.attr("font-size", 12)
}

// Render ddl node
{
let circle = gSel.select<SVGCircleElement>("circle")
if (circle.empty()) {
circle = gSel.append("circle")
}

circle.attr("r", 20).attr("fill", (_) => {
const weight = "500"
return theme.colors.gray[weight]
})

circle
.attr("cx", ddlLayoutX / 2)
.attr("cy", ddlLayoutY / 2)
.attr("fill", "white")
.attr("stroke-width", 1)
.attr("stroke", theme.colors.gray[500])
}
}

const createDdl = (sel: Enter<DdlSelection>) =>
sel.append("g").attr("class", "ddl").call(applyDdl)

const ddlSelection = svgSelection
.select<SVGGElement>(".ddls")
.selectAll<SVGGElement, null>(".ddl")
.data(ddlLayout)
type DdlSelection = typeof ddlSelection

ddlSelection.enter().call(createDdl)
// TODO(kwannoel): Is this even needed? I commented it out.
// ddlSelection.call(applyDdl)
kwannoel marked this conversation as resolved.
Show resolved Hide resolved
ddlSelection.exit().remove()

// Ddl Edges
const edgeSelection = svgSelection
.select<SVGGElement>(".ddl-edges")
.selectAll<SVGGElement, null>(".ddl-edge")
.data(ddlEdgeLayout)
type EdgeSelection = typeof edgeSelection

const curveStyle = d3.curveMonotoneX

const line = d3
.line<Position>()
.curve(curveStyle)
.x(({ x }) => x)
.y(({ y }) => y)

const applyEdge = (gSel: EdgeSelection) => {
// Edge line
let path = gSel.select<SVGPathElement>("path")
if (path.empty()) {
path = gSel.append("path")
}

const color = (d: Edge) => {
if (backPressures) {
let value = backPressures.get(`${d.target}_${d.source}`)
if (value) {
return backPressureColor(value)
}
}

return theme.colors.gray["300"]
}

const width = (d: Edge) => {
if (backPressures) {
let value = backPressures.get(`${d.target}_${d.source}`)
if (value) {
return backPressureWidth(value)
}
}

return 2
}

path
.attr("d", ({ points }) => line(points))
.attr("fill", "none")
.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)}%`
}
}

return ""
}

title.text(text)

return gSel
}
const createEdge = (sel: Enter<EdgeSelection>) =>
sel.append("g").attr("class", "ddl-edge").call(applyEdge)

edgeSelection.enter().call(createEdge)
edgeSelection.call(applyEdge)
edgeSelection.exit().remove()
}
}, [ddlLayout, ddlEdgeLayout, backPressures])

return (
<Fragment>
<svg ref={svgRef} width={`${svgWidth}px`} height={`${svgHeight}px`}>
<g className="ddl-edges" />
<g className="ddls" />
</svg>
</Fragment>
)
}

/**
* The color for the edge with given back pressure value.
*
* @param value The back pressure rate, between 0 and 100.
*/
function backPressureColor(value: number) {
const colorRange = [
theme.colors.green["100"],
theme.colors.green["300"],
theme.colors.yellow["400"],
theme.colors.orange["500"],
theme.colors.red["700"],
].map((c) => tinycolor(c))

value = Math.max(value, 0)
value = Math.min(value, 100)

const step = colorRange.length - 1
const pos = (value / 100) * step
const floor = Math.floor(pos)
const ceil = Math.ceil(pos)

const color = tinycolor(colorRange[floor])
.mix(tinycolor(colorRange[ceil]), (pos - floor) * 100)
.toHexString()

return color
}

/**
* The width for the edge with given back pressure value.
*
* @param value The back pressure rate, between 0 and 100.
*/
function backPressureWidth(value: number) {
value = Math.max(value, 0)
value = Math.min(value, 100)

return 15 * (value / 100) + 2
}
1 change: 1 addition & 0 deletions dashboard/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function Layout({ children }: { children: React.ReactNode }) {
<NavTitle>Streaming</NavTitle>
<NavButton href="/dependency_graph/">Dependency Graph</NavButton>
<NavButton href="/fragment_graph/">Fragment Graph</NavButton>
<NavButton href="/ddl_graph/">Ddl Graph</NavButton>
</Section>
<Section>
<NavTitle>Batch</NavTitle>
Expand Down
5 changes: 4 additions & 1 deletion dashboard/lib/api/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export default function useFetch<T>(
const [response, setResponse] = useState<T>()
const toast = useErrorToast()

// NOTE(eric): Don't put `fetchFn` in the dependency array. It might be a lambda function
useEffect(() => {
const fetchData = async () => {
if (when) {
Expand All @@ -53,6 +52,10 @@ export default function useFetch<T>(

const timer = setInterval(fetchData, intervalMs)
return () => clearInterval(timer)
// NOTE(eric): Don't put `fetchFn` in the dependency array. Otherwise, it can cause an infinite loop.
// This is because `fetchFn` can be recreated every render, then it will trigger a dependency change,
// which triggers a re-render, and so on.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [toast, intervalMs, when])

return { response }
Expand Down
9 changes: 9 additions & 0 deletions dashboard/lib/api/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
View,
} from "../../proto/gen/catalog"
import {
FragmentVertexToRelationMap,
ListObjectDependenciesResponse_ObjectDependencies as ObjectDependencies,
RelationIdInfos,
TableFragments,
Expand Down Expand Up @@ -130,6 +131,13 @@ export async function getRelationDependencies() {
return await getObjectDependencies()
}

export async function getFragmentVertexToRelationMap() {
let res = await api.get("/fragment_vertex_to_relation_id_map")
let fragmentVertexToRelationMap: FragmentVertexToRelationMap =
FragmentVertexToRelationMap.fromJSON(res)
return fragmentVertexToRelationMap
}

async function getTableCatalogsInner(
path: "tables" | "materialized_views" | "indexes" | "internal_tables"
) {
Expand Down Expand Up @@ -200,6 +208,7 @@ export async function getSchemas() {
return schemas
}

// Returns a map of object id to a list of object ids that it depends on
export async function getObjectDependencies() {
let objDependencies: ObjectDependencies[] = (
await api.get("/object_dependencies")
Expand Down
Loading
Loading