From c743a9afe4e74200ebda78de97f03585d2ca1e36 Mon Sep 17 00:00:00 2001 From: Bugen Zhao Date: Fri, 5 Jan 2024 16:57:20 +0800 Subject: [PATCH] cleanup dead codes Signed-off-by: Bugen Zhao --- dashboard/components/FragmentGraph.tsx | 2 +- dashboard/components/StatusLamp.js | 29 - dashboard/lib/color.js | 90 -- dashboard/lib/graaphEngine/canvasEngine.js | 598 ----------- dashboard/lib/graaphEngine/svgEngine.js | 198 ---- dashboard/lib/str.js | 24 - dashboard/lib/streamPlan/parser.js | 428 -------- dashboard/lib/streamPlan/streamChartHelper.js | 945 ------------------ dashboard/test/algo.test.js | 109 -- 9 files changed, 1 insertion(+), 2422 deletions(-) delete mode 100644 dashboard/components/StatusLamp.js delete mode 100644 dashboard/lib/color.js delete mode 100644 dashboard/lib/graaphEngine/canvasEngine.js delete mode 100644 dashboard/lib/graaphEngine/svgEngine.js delete mode 100644 dashboard/lib/str.js delete mode 100644 dashboard/lib/streamPlan/parser.js delete mode 100644 dashboard/lib/streamPlan/streamChartHelper.js delete mode 100644 dashboard/test/algo.test.js diff --git a/dashboard/components/FragmentGraph.tsx b/dashboard/components/FragmentGraph.tsx index 6ed35c536b47e..d9ca56fea69e8 100644 --- a/dashboard/components/FragmentGraph.tsx +++ b/dashboard/components/FragmentGraph.tsx @@ -265,7 +265,7 @@ export default function FragmentGraph({ .append("path") .attr("fill", "none") .attr("stroke", theme.colors.gray[700]) - .attr("stroke-width", 1.5) // TODO + .attr("stroke-width", 1.5) .call(applyEdge) return sel } diff --git a/dashboard/components/StatusLamp.js b/dashboard/components/StatusLamp.js deleted file mode 100644 index 6cadd9e2e7764..0000000000000 --- a/dashboard/components/StatusLamp.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -export default function StatusLamp(props) { - return ( -
- ) -} diff --git a/dashboard/lib/color.js b/dashboard/lib/color.js deleted file mode 100644 index 8b6d7d0fccd6a..0000000000000 --- a/dashboard/lib/color.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -const two = [ - ["#6FE0D3", "#E09370"], - ["#75D0E0", "#E0A575"], - ["#75BAE0", "#E0B175"], - ["#77A5E0", "#E0BC77"], - ["#768CE0", "#E0C577"], - ["#7575E0", "#E0CD75"], - ["#A978E0", "#E0DF79"], - ["#C977E0", "#B9E077"], - ["#E072D7", "#92E072"], - ["#E069A4", "#69E069"], - ["#E06469", "#65E086"], - ["#E07860", "#60E0B2"], - ["#E08159", "#5AE0CE"], - ["#E09C5C", "#5CC5E0"], - ["#E0B763", "#6395E0"], - ["#E0CE5A", "#6B5AE0"], - ["#C8E051", "#AA51E0"], - ["#92E06F", "#E070DB"], - ["#79E085", "#E07998"], - ["#80E0B1", "#E08C80"], - ["#91DBE0", "#E0B292"], -] - -const twoGradient = [ - ["#1976d2", "#a6c9ff"], - ["#FFF38A", "#E0D463"], - ["#A3ACFF", "#7983DF"], - ["#A6C9FF", "#7BA3DF"], - ["#FFBE8C", "#E09B65"], - ["#FFD885", "#E0B65D"], - ["#9EE2FF", "#73BEDF"], - ["#DAFF8F", "#B8E066"], - ["#FFC885", "#E0A65D"], - ["#9EFCFF", "#74DCDF"], - ["#FBFF8C", "#DBE065"], - ["#9CFFDE", "#71DFBB"], - ["#FFAF91", "#E08869"], - ["#B699FF", "#9071E0"], - ["#9EFFB6", "#74DF8F"], - ["#FFA19C", "#E07872"], - ["#AEFF9C", "#85DF71"], - ["#FF96B9", "#E06D94"], - ["#FFE785", "#E0C75F"], - ["#FF94FB", "#E06BDC"], - ["#DA99FF", "#B66FE0"], - ["#8F93FF", "#666AE0"], -] - -const five = [ - ["#A8936C", "#F5D190", "#8B84F5", "#9AA84A", "#E1F578"], - ["#A87C6A", "#F5AB8E", "#82CBF5", "#A89348", "#F5D876"], - ["#A87490", "#F59DCB", "#90F5C7", "#A87752", "#F5B584"], - ["#856FA8", "#B995F5", "#BAF58A", "#A84D5B", "#F57D8E"], - ["#7783A8", "#A2B4F5", "#F5EE95", "#9C56A8", "#E589F5"], - ["#74A895", "#9DF5D4", "#F5BF91", "#526CA8", "#84A6F5"], - ["#74A878", "#9DF5A3", "#F5A290", "#5298A8", "#84DFF5"], - ["#94A877", "#D2F5A2", "#F596B6", "#56A88C", "#89F5D0"], - ["#A8A072", "#F5E79A", "#CD8DF5", "#5DA851", "#92F582"], - ["#A89176", "#F5CD9F", "#92A3F5", "#A8A554", "#F5F087"], - ["#A8726A", "#F59B8E", "#83ECF5", "#A88948", "#F5CB76"], -] -export function TwoColor(index) { - return two[index % two.length] -} - -export function FiveColor(index) { - return five[index % five.length] -} - -let s = Math.random() * 100 -export function TwoGradient(index) { - return twoGradient[(Math.round(s) + index) % two.length] -} diff --git a/dashboard/lib/graaphEngine/canvasEngine.js b/dashboard/lib/graaphEngine/canvasEngine.js deleted file mode 100644 index df661b53c5ec6..0000000000000 --- a/dashboard/lib/graaphEngine/canvasEngine.js +++ /dev/null @@ -1,598 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -import { fabric } from "fabric" - -// Disable cache to improve performance. -fabric.Object.prototype.objectCaching = false -fabric.Object.prototype.statefullCache = false -fabric.Object.prototype.noScaleCache = true -fabric.Object.prototype.needsItsOwnCache = () => false - -export class DrawElement { - /** - * @param {{svgElement: d3.Selection}} props - */ - constructor(props) { - /** - * @type {{svgElement: d3.Selection}} - */ - this.props = props - if (props.canvasElement) { - props.engine.canvas.add(props.canvasElement) - props.canvasElement.on("mouse:down", (e) => { - console.log(e) - }) - } - - this.eventHandler = new Map() - } - - // TODO: this method is for migrating from d3.js to fabric.js. - // This should be replaced by a more suitable way. - _attrMap(key, value) { - return [key, value] - } - - // TODO: this method is for migrating from d3.js to fabric.js. - // This should be replaced by a more suitable way. - attr(key, value) { - let setting = this._attrMap(key, value) - if (setting && setting.length === 2) { - this.props.canvasElement && - this.props.canvasElement.set(setting[0], setting[1]) - } - return this - } - - _afterPosition() { - let ele = this.props.canvasElement - ele && this.props.engine._addDrawElement(this) - } - - // TODO: this method is for migrating from d3.js to fabric.js. - // This should be replaced by a more suitable way. - position(x, y) { - this.props.canvasElement.set("left", x) - this.props.canvasElement.set("top", y) - this._afterPosition() - return this - } - - on(event, callback) { - this.eventHandler.set(event, callback) - return this - } - - getEventHandler(event) { - return this.eventHandler.get(event) - } - - // TODO: this method is for migrating from d3.js to fabric.js. - // This should be replaced by a more suitable way. - style(key, value) { - return this.attr(key, value) - } - - classed(clazz, flag) { - this.props.engine.classedElement(clazz, this, flag) - return this - } -} - -export class Group extends DrawElement { - /** - * @param {{engine: CanvasEngine}} props - */ - constructor(props) { - super(props) - - this.appendFunc = { - g: this._appendGroup, - circle: this._appendCircle, - rect: this._appendRect, - text: this._appendText, - path: this._appendPath, - polygon: this._appendPolygan, - } - - this.basicSetting = { - engine: props.engine, - } - } - - _appendGroup = () => { - return new Group(this.basicSetting) - } - - _appendCircle = () => { - return new Circle({ - ...this.basicSetting, - ...{ - canvasElement: new fabric.Circle({ - selectable: false, - hoverCursor: "pointer", - }), - }, - }) - } - - _appendRect = () => { - return new Rectangle({ - ...this.basicSetting, - ...{ - canvasElement: new fabric.Rect({ - selectable: false, - hoverCursor: "pointer", - }), - }, - }) - } - - _appendText = () => { - return (content) => - new Text({ - ...this.basicSetting, - ...{ - canvasElement: new fabric.Text(content || "undefined", { - selectable: false, - textAlign: "justify-center", - }), - }, - }) - } - - _appendPath = () => { - return (d) => - new Path({ - ...this.basicSetting, - ...{ - canvasElement: new fabric.Path(d, { selectable: false }), - }, - }) - } - - _appendPolygan = () => { - return new Polygan(this.basicSetting) - } - - append = (type) => { - return this.appendFunc[type]() - } -} - -export class Rectangle extends DrawElement { - /** - * @param {{g: fabric.Group}} props - */ - constructor(props) { - super(props) - this.props = props - } - - init(x, y, width, height) { - let ele = this.props.canvasElement - ele.set("left", x) - ele.set("top", y) - ele.set("width", width) - ele.set("height", height) - super._afterPosition() - return this - } - - _attrMap(key, value) { - if (key === "rx") { - this.props.canvasElement.set("rx", value) - this.props.canvasElement.set("ry", value) - return false - } - return [key, value] - } -} - -export class Circle extends DrawElement { - /** - * @param {{svgElement: d3.Selection}} props - */ - constructor(props) { - super(props) - this.props = props - this.radius = 0 - } - - init(x, y, r) { - this.props.canvasElement.set("left", x - r) - this.props.canvasElement.set("top", y - r) - this.props.canvasElement.set("radius", r) - super._afterPosition() - return this - } - - _attrMap(key, value) { - if (key === "r") { - this.radius = value - return ["radius", value] - } - if (key === "cx") { - return ["left", value - this.radius] - } - if (key === "cy") { - return ["top", value - this.radius] - } - return [key, value] - } -} - -export class Text extends DrawElement { - /** - * @param {{svgElement: d3.Selection, any, null, undefined>}} props - */ - constructor(props) { - super(props) - this.props = props - } - - position(x, y) { - let e = this.props.canvasElement - e.set("top", y) - e.set("left", x) - super._afterPosition() - return this - } - - _attrMap(key, value) { - if (key === "text-anchor") { - return ["textAlign", value] - } - if (key === "font-size") { - return ["fontSize", value] - } - return [key, value] - } - - text(content) { - return this - } - - getWidth() {} -} - -export class Polygan extends DrawElement { - constructor(props) { - super(props) - this.props = props - } -} - -export class Path extends DrawElement { - constructor(props) { - super(props) - this.props = props - this.strokeWidth = 1 - super._afterPosition() - } - - _attrMap(key, value) { - if (key === "fill") { - return ["fill", value === "none" ? false : value] - } - if (key === "stroke-width") { - this.props.canvasElement.set( - "top", - this.props.canvasElement.get("top") - value / 2 - ) - return ["strokeWidth", value] - } - if (key === "stroke-dasharray") { - return ["strokeDashArray", value.split(",")] - } - if (key === "layer") { - if (value === "back") { - this.props.canvasElement.canvas.sendToBack(this.props.canvasElement) - } - return false - } - return [key, value] - } -} - -// TODO: Use rbtree -class CordMapper { - constructor() { - this.map = new Map() - } - - rangeQuery(start, end) { - let rtn = new Set() - for (let [k, s] of this.map.entries()) { - if (start <= k && k <= end) { - s.forEach((v) => rtn.add(v)) - } - } - return rtn - } - - insert(k, v) { - if (this.map.has(k)) { - this.map.get(k).add(v) - } else { - this.map.set(k, new Set([v])) - } - } -} - -class GridMapper { - constructor() { - this.xMap = new CordMapper() - this.yMap = new CordMapper() - this.gs = 100 // grid size - } - - _getKey(value) { - return Math.round(value / this.gs) - } - - addObject(minX, maxX, minY, maxY, ele) { - for (let i = minX; i <= maxX + this.gs; i += this.gs) { - this.xMap.insert(this._getKey(i), ele) - } - for (let i = minY; i <= maxY + this.gs; i += this.gs) { - this.yMap.insert(this._getKey(i), ele) - } - } - - areaQuery(minX, maxX, minY, maxY) { - let xs = this.xMap.rangeQuery(this._getKey(minX), this._getKey(maxX)) - let ys = this.yMap.rangeQuery(this._getKey(minY), this._getKey(maxY)) - let rtn = new Set() - xs.forEach((e) => { - if (ys.has(e)) { - rtn.add(e) - } - }) - return rtn - } -} - -export class CanvasEngine { - /** - * @param {string} canvasId The DOM id of the canvas - * @param {number} height the height of the canvas - * @param {number} width the width of the canvas - */ - constructor(canvasId, height, width) { - let canvas = new fabric.Canvas(canvasId) - canvas.selection = false // improve performance - - this.height = height - this.width = width - this.canvas = canvas - this.clazzMap = new Map() - this.topGroup = new Group({ engine: this }) - this.gridMapper = new GridMapper() - this.canvasElementToDrawElement = new Map() - - let that = this - canvas.on("mouse:wheel", function (opt) { - var evt = opt.e - if (evt.ctrlKey === true) { - var delta = opt.e.deltaY - var zoom = canvas.getZoom() - zoom *= 0.999 ** delta - if (zoom > 10) zoom = 10 - if (zoom < 0.03) zoom = 0.03 - canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom) - that._refreshView() - evt.preventDefault() - evt.stopPropagation() - } else { - that.moveCamera(-evt.deltaX, -evt.deltaY) - evt.preventDefault() - evt.stopPropagation() - } - }) - - canvas.on("mouse:down", function (opt) { - var evt = opt.e - this.isDragging = true - this.selection = false - this.lastPosX = evt.clientX - this.lastPosY = evt.clientY - - that._handleClickEvent(opt.target) - }) - - canvas.on("mouse:move", function (opt) { - if (this.isDragging) { - var e = opt.e - that.moveCamera(e.clientX - this.lastPosX, e.clientY - this.lastPosY) - this.lastPosX = e.clientX - this.lastPosY = e.clientY - } - }) - canvas.on("mouse:up", function (opt) { - this.setViewportTransform(this.viewportTransform) - this.isDragging = false - this.selection = true - }) - } - - /** - * Move the current view point. - * @param {number} deltaX - * @param {number} deltaY - */ - async moveCamera(deltaX, deltaY) { - this.canvas.setZoom(this.canvas.getZoom()) // essential for rendering (seems like a bug) - let vpt = this.canvas.viewportTransform - vpt[4] += deltaX - vpt[5] += deltaY - this._refreshView() - } - - /** - * Invoke the click handler of an object. - * @param {fabric.Object} target - */ - async _handleClickEvent(target) { - if (target === null) { - return - } - let ele = this.canvasElementToDrawElement.get(target) - let func = ele.getEventHandler("click") - if (func) { - func() - } - } - - /** - * Set the objects in the current view point visible. - * And set other objects not visible. - */ - async _refreshView() { - const padding = 50 // Make the rendering area a little larger. - let vpt = this.canvas.viewportTransform - let zoom = this.canvas.getZoom() - let cameraWidth = this.width - let cameraHeight = this.height - let minX = -vpt[4] - padding - let maxX = -vpt[4] + cameraWidth + padding - let minY = -vpt[5] - padding - let maxY = -vpt[5] + cameraHeight + padding - let visibleSet = this.gridMapper.areaQuery( - minX / zoom, - maxX / zoom, - minY / zoom, - maxY / zoom - ) - - this.canvas.getObjects().forEach((e) => { - if (visibleSet.has(e)) { - e.visible = true - } else { - e.visible = false - } - }) - - this.canvas.requestRenderAll() - } - - /** - * Register an element to the engine. This should - * be called when a DrawElement instance is added - * to the canvas. - * @param {DrawElement} ele - */ - _addDrawElement(ele) { - let canvasElement = ele.props.canvasElement - this.canvasElementToDrawElement.set(canvasElement, ele) - this.gridMapper.addObject( - canvasElement.left, - canvasElement.left + canvasElement.width, - canvasElement.top, - canvasElement.top + canvasElement.height, - canvasElement - ) - } - - /** - * Assign a class to an object or remove a class from it. - * @param {string} clazz class name - * @param {DrawElement} element target object - * @param {boolean} flag true if the object is assigned, otherwise - * remove the class from the object - */ - classedElement(clazz, element, flag) { - if (!flag) { - this.clazzMap.has(clazz) && this.clazzMap.get(clazz).delete(element) - } else { - if (this.clazzMap.has(clazz)) { - this.clazzMap.get(clazz).add(element) - } else { - this.clazzMap.set(clazz, new Set([element])) - } - } - } - - /** - * Move current view point to the object specified by - * the selector. The selector is the class of the - * target object for now. - * @param {string} selector The class of the target object - */ - locateTo(selector) { - // - let selectorSet = this.clazzMap.get(selector) - if (selectorSet) { - let arr = Array.from(selectorSet) - if (arr.length > 0) { - let ele = arr[0] - let x = ele.props.canvasElement.get("left") - let y = ele.props.canvasElement.get("top") - let scale = 0.6 - this.canvas.setZoom(scale) - let vpt = this.canvas.viewportTransform - vpt[4] = (-x + this.width * 0.5) * scale - vpt[5] = (-y + this.height * 0.5) * scale - this.canvas.requestRenderAll() - this._refreshView() - } - } - } - - /** - * Move current view point to (0, 0) - */ - resetCamera() { - let zoom = this.canvas.getZoom() - zoom *= 0.999 - this.canvas.setZoom(zoom) - let vpt = this.canvas.viewportTransform - vpt[4] = 0 - vpt[5] = 0 - this.canvas.requestRenderAll() - this._refreshView() - } - - /** - * Dispose the current canvas. Remove all the objects to - * free memory. All objects in the canvas will be removed. - */ - cleanGraph() { - console.log("clean called") - this.canvas.dispose() - } - - /** - * Resize the canvas. This is called when the browser size - * is changed, such that the canvas can fix the current - * size of the browser. - * - * Note that the outer div box of the canvas will be set - * according to the parameters. However, the width and - * height of the canvas is double times of the parameters. - * This is the feature of fabric.js to keep the canvas - * in high resolution all the time. - * - * @param {number} width the width of the canvas - * @param {number} height the height of the canvas - */ - resize(width, height) { - this.width = width - this.height = height - this.canvas.setDimensions({ width: this.width, height: this.height }) - } -} diff --git a/dashboard/lib/graaphEngine/svgEngine.js b/dashboard/lib/graaphEngine/svgEngine.js deleted file mode 100644 index 8102b79df0a4b..0000000000000 --- a/dashboard/lib/graaphEngine/svgEngine.js +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -import * as d3 from "d3" - -export class DrawElement { - /** - * @param {{svgElement: d3.Selection}} props - */ - constructor(props) { - /** - * @type {{svgElement: d3.Selection}} - */ - this.props = props - this.appendFunc = { - g: this._appendGroup, - circle: this._appendCircle, - rect: this._appendRect, - text: this._appendText, - path: this._appendPath, - polygon: this._appendPolygan, - } - } - - _appendGroup = () => { - return new Group({ svgElement: this.props.svgElement.append("g") }) - } - - _appendCircle = () => { - return new Circle({ svgElement: this.props.svgElement.append("circle") }) - } - - _appendRect = () => { - return new Rectangle({ svgElement: this.props.svgElement.append("rect") }) - } - - _appendText = () => { - return new Text({ svgElement: this.props.svgElement.append("text") }) - } - - _appendPath = () => { - return new Path({ svgElement: this.props.svgElement.append("path") }) - } - - _appendPolygan = () => { - return new Polygan({ svgElement: this.props.svgElement.append("polygon") }) - } - - on = (event, callback) => { - this.props.svgElement.on(event, callback) - return this - } - - style = (key, value) => { - this.props.svgElement.style(key, value) - return this - } - - classed = (clazz, flag) => { - this.props.svgElement.classed(clazz, flag) - return this - } - - attr = (key, value) => { - this.props.svgElement.attr(key, value) - return this - } - - append = (type) => { - return this.appendFunc[type]() - } -} - -export class Group extends DrawElement { - /** - * @param {{svgElement: d3.Selection}} props - */ - constructor(props) { - super(props) - } -} - -export class Rectangle extends DrawElement { - /** - * @param {{svgElement: d3.Selection}} props - */ - constructor(props) { - super(props) - } -} - -export class Circle extends DrawElement { - /** - * @param {{svgElement: d3.Selection}} props - */ - constructor(props) { - super(props) - } -} - -export class Text extends DrawElement { - /** - * @param {{svgElement: d3.Selection, any, null, undefined>}} props - */ - constructor(props) { - super(props) - this.props = props - } - - text(content) { - this.props.svgElement.text(content) - return this - } - - getWidth() { - return this.props.svgElement.node().getComputedTextLength() - } -} - -export class Polygan extends DrawElement { - constructor(props) { - super(props) - this.props = props - } -} - -export class Path extends DrawElement { - constructor(props) { - super(props) - } -} - -const originalZoom = new d3.ZoomTransform(0.5, 0, 0) - -export class SvgEngine { - /** - * @param {{g: d3.Selection}} props - */ - constructor(svgRef, height, width) { - this.height = height - this.width = width - this.svgRef = svgRef - - d3.select(svgRef).selectAll("*").remove() - this.svg = d3.select(svgRef).attr("viewBox", [0, 0, width, height]) - - this._g = this.svg.append("g").attr("class", "top") - this.topGroup = new Group({ svgElement: this._g }) - - this.transform - this.zoom = d3.zoom().on("zoom", (e) => { - this.transform = e.transform - this._g.attr("transform", e.transform) - }) - - this.svg.call(this.zoom).call(this.zoom.transform, originalZoom) - this.svg.on("pointermove", (event) => { - this.transform.invert(d3.pointer(event)) - }) - } - - locateTo(selector) { - let selection = d3.select(selector) - if (!selection.empty()) { - this.svg - .call(this.zoom) - .call( - this.zoom.transform, - new d3.ZoomTransform( - 0.7, - -0.7 * selection.attr("x"), - 0.7 * (-selection.attr("y") + this.height / 2) - ) - ) - } - } - - resetCamera() { - this.svg.call(this.zoom.transform, originalZoom) - } - - cleanGraph() { - d3.select(this.svgRef).selectAll("*").remove() - } -} diff --git a/dashboard/lib/str.js b/dashboard/lib/str.js deleted file mode 100644 index 52af7892f4a75..0000000000000 --- a/dashboard/lib/str.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -export function capitalize(sentence) { - let words = sentence.split(" ") - let s = "" - for (let word of words) { - s += word.charAt(0).toUpperCase() + word.slice(1, word.length) - } - return s -} diff --git a/dashboard/lib/streamPlan/parser.js b/dashboard/lib/streamPlan/parser.js deleted file mode 100644 index 050e0f05619a4..0000000000000 --- a/dashboard/lib/streamPlan/parser.js +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -import { graphBfs } from "../algo" - -let cnt = 0 -function generateNewNodeId() { - return "g" + ++cnt -} - -function getNodeId(nodeProto, actorId) { - return ( - actorId + - ":" + - (nodeProto.operatorId === undefined - ? generateNewNodeId() - : "o" + nodeProto.operatorId) - ) -} - -class Node { - constructor(id, actorId, nodeProto) { - this.id = id - /** - * @type {any} - */ - this.nodeProto = nodeProto - /** - * @type {Array} - */ - this.output = output - /** - * @type {Node} - */ - this.rootNode = rootNode - /** - * @type {number} - */ - this.fragmentId = fragmentId - /** - * @type {string} - */ - this.computeNodeAddress = computeNodeAddress - /** - * @type {Array} - */ - this.representedActorList = null - /** - * @type {Set} - */ - this.representedWorkNodes = null - } -} - -export default class StreamPlanParser { - /** - * - * @param {[{node: any, actors: []}]} data raw response from the meta node - */ - constructor(data, shownActorList) { - this.actorId2Proto = new Map() - /** - * @type {Set} - * @private - */ - this.actorIdTomviewNodes = new Map() - this.shownActorSet = new Set(shownActorList) - - for (let computeNodeData of data) { - for (let singleActorProto of computeNodeData.actors) { - if ( - shownActorList && - !this.shownActorSet.has(singleActorProto.actorId) - ) { - continue - } - this.actorId2Proto.set(singleActorProto.actorId, { - computeNodeAddress: `${computeNodeData.node.host.host}:${computeNodeData.node.host.port}`, - ...singleActorProto, - }) - } - } - - this.parsedNodeMap = new Map() - this.parsedActorMap = new Map() - - for (let [_, singleActorProto] of this.actorId2Proto.entries()) { - this.parseActor(singleActorProto) - } - - this.parsedActorList = [] - for (let [_, actor] of this.parsedActorMap.entries()) { - this.parsedActorList.push(actor) - } - - /** @type {Set} */ - this.fragmentRepresentedActors = this._constructRepresentedActorList() - - /** @type {Map} */ - this.mvTableIdToSingleViewActorList = this._constructSingleViewMvList() - - /** @type {Map} */ - this.mvTableIdToChainViewActorList = this._constructChainViewMvList() - } - - /** - * Randomly select a actor to represent its - * fragment, and append a property named `representedActorList` - * to store all the other actors in the same fragment. - * - * Actors are degree of parallelism of a fragment, such that one of - * the actor in a fragment can represent all the other actor in - * the same fragment. - * - * @returns A Set containing actors representing its fragment. - */ - _constructRepresentedActorList() { - const fragmentId2actorList = new Map() - let fragmentRepresentedActors = new Set() - for (let actor of this.parsedActorList) { - if (!fragmentId2actorList.has(actor.fragmentId)) { - fragmentRepresentedActors.add(actor) - fragmentId2actorList.set(actor.fragmentId, [actor]) - } else { - fragmentId2actorList.get(actor.fragmentId).push(actor) - } - } - - for (let actor of fragmentRepresentedActors) { - actor.representedActorList = fragmentId2actorList - .get(actor.fragmentId) - .sort((x) => x.actorId) - actor.representedWorkNodes = new Set() - for (let representedActor of actor.representedActorList) { - representedActor.representedActorList = actor.representedActorList - actor.representedWorkNodes.add(representedActor.computeNodeAddress) - } - } - return fragmentRepresentedActors - } - - _constructChainViewMvList() { - let mvTableIdToChainViewActorList = new Map() - let shellNodes = new Map() - const getShellNode = (actorId) => { - if (shellNodes.has(actorId)) { - return shellNodes.get(actorId) - } - let shellNode = { - id: actorId, - parentNodes: [], - nextNodes: [], - } - for (let node of this.parsedActorMap.get(actorId).output) { - let nextNode = getShellNode(node.actorId) - nextNode.parentNodes.push(shellNode) - shellNode.nextNodes.push(nextNode) - } - shellNodes.set(actorId, shellNode) - return shellNode - } - - for (let actorId of this.actorId2Proto.keys()) { - getShellNode(actorId) - } - - for (let [actorId, mviewNode] of this.actorIdTomviewNodes.entries()) { - let list = new Set() - let shellNode = getShellNode(actorId) - graphBfs(shellNode, (n) => { - list.add(n.id) - }) - graphBfs( - shellNode, - (n) => { - list.add(n.id) - }, - "parentNodes" - ) - for (let actor of this.parsedActorMap.get(actorId).representedActorList) { - list.add(actor.actorId) - } - mvTableIdToChainViewActorList.set(mviewNode.typeInfo.tableId, [ - ...list.values(), - ]) - } - - return mvTableIdToChainViewActorList - } - - _constructSingleViewMvList() { - let mvTableIdToSingleViewActorList = new Map() - let shellNodes = new Map() - const getShellNode = (actorId) => { - if (shellNodes.has(actorId)) { - return shellNodes.get(actorId) - } - let shellNode = { - id: actorId, - parentNodes: [], - } - for (let node of this.parsedActorMap.get(actorId).output) { - getShellNode(node.actorId).parentNodes.push(shellNode) - } - shellNodes.set(actorId, shellNode) - return shellNode - } - for (let actor of this.parsedActorList) { - getShellNode(actor.actorId) - } - - for (let actorId of this.actorId2Proto.keys()) { - getShellNode(actorId) - } - - for (let [actorId, mviewNode] of this.actorIdTomviewNodes.entries()) { - let list = [] - let shellNode = getShellNode(actorId) - graphBfs( - shellNode, - (n) => { - list.push(n.id) - if (shellNode.id !== n.id && this.actorIdTomviewNodes.has(n.id)) { - return true // stop to traverse its next nodes - } - }, - "parentNodes" - ) - for (let actor of this.parsedActorMap.get(actorId).representedActorList) { - list.push(actor.actorId) - } - mvTableIdToSingleViewActorList.set(mviewNode.typeInfo.tableId, list) - } - - return mvTableIdToSingleViewActorList - } - - newDispatcher(actorId, type, downstreamActorId) { - return new Dispatcher(actorId, type, downstreamActorId, { - operatorId: 100000 + actorId, - }) - } - - /** - * Parse raw data from meta node to an actor - * @param {{ - * actorId: number, - * fragmentId: number, - * nodes: any, - * dispatcher?: {type: string}, - * downstreamActorId?: any - * }} actorProto - * @returns {Actor} - */ - parseActor(actorProto) { - let actorId = actorProto.actorId - if (this.parsedActorMap.has(actorId)) { - return this.parsedActorMap.get(actorId) - } - - let actor = new Actor( - actorId, - [], - null, - actorProto.fragmentId, - actorProto.computeNodeAddress - ) - - let rootNode - this.parsedActorMap.set(actorId, actor) - if (actorProto.dispatcher && actorProto.dispatcher[0].type) { - let nodeBeforeDispatcher = this.parseNode(actor.actorId, actorProto.nodes) - rootNode = this.newDispatcher( - actor.actorId, - actorProto.dispatcher[0].type, - actorProto.downstreamActorId - ) - rootNode.nextNodes = [nodeBeforeDispatcher] - } else { - rootNode = this.parseNode(actorId, actorProto.nodes) - } - actor.rootNode = rootNode - - return actor - } - - parseNode(actorId, nodeProto) { - let id = getNodeId(nodeProto, actorId) - if (this.parsedNodeMap.has(id)) { - return this.parsedNodeMap.get(id) - } - let newNode = new StreamNode(id, actorId, nodeProto) - this.parsedNodeMap.set(id, newNode) - - if (nodeProto.input !== undefined) { - for (let nextNodeProto of nodeProto.input) { - newNode.nextNodes.push(this.parseNode(actorId, nextNodeProto)) - } - } - - if (newNode.type === "merge" && newNode.typeInfo.upstreamActorId) { - for (let upStreamActorId of newNode.typeInfo.upstreamActorId) { - if (!this.actorId2Proto.has(upStreamActorId)) { - continue - } - this.parseActor(this.actorId2Proto.get(upStreamActorId)).output.push( - newNode - ) - } - } - - if (newNode.type === "streamScan" && newNode.typeInfo.upstreamActorIds) { - for (let upStreamActorId of newNode.typeInfo.upstreamActorIds) { - if (!this.actorId2Proto.has(upStreamActorId)) { - continue - } - this.parseActor(this.actorId2Proto.get(upStreamActorId)).output.push( - newNode - ) - } - } - - if (newNode.type === "materialize") { - this.actorIdTomviewNodes.set(actorId, newNode) - } - - return newNode - } - - getActor(actorId) { - return this.parsedActorMap.get(actorId) - } - - getOperator(operatorId) { - return this.parsedNodeMap.get(operatorId) - } - - /** - * @returns {Array} - */ - getParsedActorList() { - return this.parsedActorList - } -} diff --git a/dashboard/lib/streamPlan/streamChartHelper.js b/dashboard/lib/streamPlan/streamChartHelper.js deleted file mode 100644 index 5610273cda3de..0000000000000 --- a/dashboard/lib/streamPlan/streamChartHelper.js +++ /dev/null @@ -1,945 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -import * as d3 from "d3" -import { cloneDeep, max } from "lodash" -import { getConnectedComponent, treeBfs } from "../algo" -import * as color from "../color" -import { Group } from "../graaphEngine/canvasEngine" -import { newNumberArray } from "../util" -import StreamPlanParser, { Actor } from "./parser" -// Actor constant -// -// ======================================================= -// ^ -// | actorBoxPadding -// v -// --┌───────────┐ -// | │ node │ -// | │<--->radius│>───────\ -// | │ │ │ -// | └───────────┘ │ -// | │ -// | ┌───────────┐ │ ┌───────────┐ -// | │ node │ │ │ node │ -// widthUnit│<--->radius│>───────┼────────>│<--->radius│ -// | │ │ │ │ │ -// | └───────────┘ │ └───────────┘ -// | │ -// | ┌───────────┐ │ -// | │ node │ │ -// | │<--->radius│>───────/ -// | │ │ -// ---└───────────┘ -// |-----------------heightUnit---------------| -// - -const SCALE_FACTOR = 0.5 - -const operatorNodeRadius = 30 * SCALE_FACTOR // the radius of the tree nodes in an actor -const operatorNodeStrokeWidth = 5 * SCALE_FACTOR // the stroke width of the link of the tree nodes in an actor -const widthUnit = 230 * SCALE_FACTOR // the width of a tree node in an actor -const heightUnit = 250 * SCALE_FACTOR // the height of a tree layer in an actor -const actorBoxPadding = 100 * SCALE_FACTOR // box padding -const actorBoxStroke = 15 * SCALE_FACTOR // the width of the stroke of the box -const internalLinkStrokeWidth = 30 * SCALE_FACTOR // the width of the link between nodes -const actorBoxRadius = 20 * SCALE_FACTOR - -// Stream Plan constant -const gapBetweenRow = 100 * SCALE_FACTOR -const gapBetweenLayer = 300 * SCALE_FACTOR -const gapBetweenFlowChart = 500 * SCALE_FACTOR -const outgoingLinkStrokeWidth = 20 * SCALE_FACTOR -const outgoingLinkBgStrokeWidth = 40 * SCALE_FACTOR - -// Draw linking effect -const bendGap = 50 * SCALE_FACTOR // try example at: http://bl.ocks.org/d3indepth/b6d4845973089bc1012dec1674d3aff8 -const connectionGap = 20 * SCALE_FACTOR - -// Others -const fontSize = 30 * SCALE_FACTOR -const outGoingLinkBgColor = "#eee" - -/** - * Construct an id for a link in actor box. - * You may use this method to query and get the svg element - * of the link. - * @param {{id: number}} node1 a node (operator) in an actor box - * @param {{id: number}} node2 a node (operator) in an actor box - * @returns {string} The link id - */ -function constructInternalLinkId(node1, node2) { - return ( - "node-" + - (node1.id > node2.id - ? node1.id + "-" + node2.id - : node2.id + "-" + node1.id) - ) -} - -/** - * Construct an id for a node (operator) in an actor box. - * You may use this method to query and get the svg element - * of the link. - * @param {{id: number}} node a node (operator) in an actor box - * @returns {string} The node id - */ -function constructOperatorNodeId(node) { - return "node-" + node.id -} - -function hashIpv4Index(addr) { - let [ip, port] = addr.split(":") - let s = "" - ip.split(".").map((x) => (s += x)) - return Number(s + port) -} - -export function computeNodeAddrToSideColor(addr) { - return color.TwoGradient(hashIpv4Index(addr))[1] -} - -/** - * Work flow - * 1. Get the layout for actor boxes (Calculate the base coordination of each actor box) - * 2. Get the layout for operators in each actor box - * 3. Draw all actor boxes - * 4. Draw link between actor boxes - * - * - * Dependencies - * layoutActorBox <- dagLayout <- drawActorBox <- drawFlow - * [ The layout of the ] [ The layout of ] [ Draw an actor ] [ Draw many actors ] - * [ operators in an ] [ actors in a ] [ in specified ] [ and links between ] - * [ actor. ] [ stream plan ] [ place ] [ them. ] - * - */ -export class StreamChartHelper { - /** - * - * @param {Group} g The group element in canvas engine - * @param {*} data The raw response from the meta node - * @param {(e, node) => void} onNodeClick The callback function triggered when a node is click - * @param {(e, actor) => void} onActorClick - * @param {{type: string, node: {host: {host: string, port: number}}, id?: number}} selectedWokerNode - * @param {Array} shownActorIdList - */ - constructor( - g, - data, - onNodeClick, - onActorClick, - selectedWokerNode, - shownActorIdList - ) { - this.topGroup = g - this.streamPlan = new StreamPlanParser(data, shownActorIdList) - this.onNodeClick = onNodeClick - this.onActorClick = onActorClick - this.selectedWokerNode = selectedWokerNode - this.selectedWokerNodeStr = this.selectedWokerNode - ? selectedWokerNode.host.host + ":" + selectedWokerNode.host.port - : "Show All" - } - - getMvTableIdToSingleViewActorList() { - return this.streamPlan.mvTableIdToSingleViewActorList - } - - getMvTableIdToChainViewActorList() { - return this.streamPlan.mvTableIdToChainViewActorList - } - - /** - * @param {Actor} actor - * @returns - */ - isInSelectedActor(actor) { - if (this.selectedWokerNodeStr === "Show All") { - // show all - return true - } else { - return actor.representedWorkNodes.has(this.selectedWokerNodeStr) - } - } - - _mainColor(actor) { - let addr = actor.representedWorkNodes.has(this.selectedWokerNodeStr) - ? this.selectedWokerNodeStr - : actor.computeNodeAddress - return color.TwoGradient(hashIpv4Index(addr))[0] - } - - _sideColor(actor) { - let addr = actor.representedWorkNodes.has(this.selectedWokerNodeStr) - ? this.selectedWokerNodeStr - : actor.computeNodeAddress - return color.TwoGradient(hashIpv4Index(addr))[1] - } - - _operatorColor = (actor, operator) => { - return this.isInSelectedActor(actor) && operator.type === "mviewNode" - ? this._mainColor(actor) - : "#eee" - } - _actorBoxBackgroundColor = (actor) => { - return this.isInSelectedActor(actor) ? this._sideColor(actor) : "#eee" - } - _actorOutgoinglinkColor = (actor) => { - return this.isInSelectedActor(actor) ? this._mainColor(actor) : "#fff" - } - - // - // A simple DAG layout algorithm. - // The layout is built based on two rules. - // 1. The link should have at two turnning points. - // 2. The turnning point of a link should be placed - // at the margin after the layer of its starting point. - // ------------------------------------------------------- - // Example 1: (X)-(Z) and (Y)-(Z) is valid. - // Row 0 (X)---------------->(Z) - // | - // Row 1 | - // | - // Row 2 (Y)---/ - // Layer 1 | Layer 2 | Layer 3 - // ------------------------------------------------------- - // Example 2: (A)-(B) is not valid. - // Row 0 (X) /---------\ (Z) - // | | - // Row 1 (A)---/ (Y) |-->(B) - // - // Layer 1 | Layer 2 | Layer 3 - // ------------------------------------------------------- - // Example 3: (C)-(Z) is not valid - // Row 0 (X) /-->(Z) - // | - // Row 1 (C)-------------/ - // - // Layer 1 | Layer 2 | Layer 3 - // ------------------------------------------------------- - // Note that the layer of each node can be different - // For example: - // Row 0 ( 1) ( 3) ( 5) ( 2) ( 9) - // Row 1 ( 4) ( 6) (10) - // Row 2 ( 7) ( 8) - // Layer 0 | Layer 1 | Layer 2 | Layer 3 | Layer 4 | - // - // Row 0 ( 1) ( 3) ( 5) ( 2) ( 9) - // Row 1 ( 4) ( 6) (10) - // Row 2 ( 7) ( 8) - // Layer 0 | Layer 1 | Layer 2 | Layer 3 | Layer 4 | - /** - * Topological sort - * @param {Array} nodes An array of node: {nextNodes: [...]} - * @returns {Map} position of each node - */ - dagLayout(nodes) { - let sorted = [] - let _nodes = [] - let node2dagNode = new Map() - const visit = (n) => { - if (n.temp) { - throw Error("This is not a DAG") - } - if (!n.perm) { - n.temp = true - let maxG = -1 - for (let nextNode of n.node.nextNodes) { - node2dagNode.get(nextNode).isInput = false - n.isOutput = false - let g = visit(node2dagNode.get(nextNode)) - if (g > maxG) { - maxG = g - } - } - n.temp = false - n.perm = true - n.g = maxG + 1 - sorted.unshift(n.node) - } - return n.g - } - for (let node of nodes) { - let dagNode = { - node: node, - temp: false, - perm: false, - isInput: true, - isOutput: true, - } - node2dagNode.set(node, dagNode) - _nodes.push(dagNode) - } - let maxLayer = 0 - for (let node of _nodes) { - let g = visit(node) - if (g > maxLayer) { - maxLayer = g - } - } - // use the bottom up strategy to construct generation number - // makes the generation number of root node the samllest - // to make the computation easier, need to flip it back. - for (let node of _nodes) { - // node.g = node.isInput ? 0 : (maxLayer - node.g); // TODO: determine which is more suitable - node.g = maxLayer - node.g - } - - let layers = [] - for (let i = 0; i < maxLayer + 1; ++i) { - layers.push({ - nodes: [], - occupyRow: new Set(), - }) - } - let node2Layer = new Map() - let node2Row = new Map() - for (let node of _nodes) { - layers[node.g].nodes.push(node.node) - node2Layer.set(node.node, node.g) - } - - // layers to rtn - let rtn = new Map() - - const putNodeInPosition = (node, row) => { - node2Row.set(node, row) - layers[node2Layer.get(node)].occupyRow.add(row) - } - - const occupyLine = (ls, le, r) => { - // layer start, layer end, row - for (let i = ls; i <= le; ++i) { - layers[i].occupyRow.add(r) - } - } - - const hasOccupied = (layer, row) => layers[layer].occupyRow.has(row) - - const isStraightLineOccupied = (ls, le, r) => { - // layer start, layer end, row - if (r < 0) { - return false - } - for (let i = ls; i <= le; ++i) { - if (hasOccupied(i, r)) { - return true - } - } - return false - } - - for (let node of nodes) { - node.nextNodes.sort((a, b) => node2Layer.get(b) - node2Layer.get(a)) - } - - for (let layer of layers) { - for (let node of layer.nodes) { - if (!node2Row.has(node)) { - // checking node is not placed. - for (let nextNode of node.nextNodes) { - if (node2Row.has(nextNode)) { - continue - } - let r = -1 - while ( - isStraightLineOccupied( - node2Layer.get(node), - node2Layer.get(nextNode), - ++r - ) - ) {} - putNodeInPosition(node, r) - putNodeInPosition(nextNode, r) - occupyLine( - node2Layer.get(node) + 1, - node2Layer.get(nextNode) - 1, - r - ) - break - } - if (!node2Row.has(node)) { - let r = -1 - while (hasOccupied(node2Layer.get(node), ++r)) {} - putNodeInPosition(node, r) - } - } - // checking node is placed in some position - for (let nextNode of node.nextNodes) { - if (node2Row.has(nextNode)) { - continue - } - // check straight line position first - let r = node2Row.get(node) - if ( - !isStraightLineOccupied( - node2Layer.get(node) + 1, - node2Layer.get(nextNode), - r - ) - ) { - putNodeInPosition(nextNode, r) - occupyLine( - node2Layer.get(node) + 1, - node2Layer.get(nextNode) - 1, - r - ) - continue - } - // check lowest available position - r = -1 - while ( - isStraightLineOccupied( - node2Layer.get(node) + 1, - node2Layer.get(nextNode), - ++r - ) - ) {} - putNodeInPosition(nextNode, r) - occupyLine(node2Layer.get(node) + 1, node2Layer.get(nextNode) - 1, r) - } - } - } - for (let node of nodes) { - rtn.set(node.id, [node2Layer.get(node), node2Row.get(node)]) - } - - return rtn - } - - /** - * Calculate the position of each node in the actor box. - * @param {{id: any, nextNodes: [], x: number, y: number}} rootNode The root node of an actor box (dispatcher) - * @returns {[width, height]} The size of the actor box - */ - calculateActorBoxSize(rootNode) { - let rootNodeCopy = cloneDeep(rootNode) - return this.layoutActorBox(rootNodeCopy, 0, 0) - } - - /** - * Calculate the position of each node (operator) in the actor box. - * This will change the node's position - * @param {{id: any, nextNodes: [], x: number, y: number}} rootNode The root node of an actor box (dispatcher) - * @param {number} baseX The x coordination of the top-left corner of the actor box - * @param {number} baseY The y coordination of the top-left corner of the actor box - * @returns {[width, height]} The size of the actor box - */ - layoutActorBox(rootNode, baseX, baseY) { - // calculate nodes' required width - let maxLayer = 0 - const getRequiredWidth = (node, layer) => { - if (node.width !== undefined) { - return node.width - } - - if (layer > maxLayer) { - maxLayer = layer - } - - node.layer = layer - - let requiredWidth = 0 - for (let nextNode of node.nextNodes) { - requiredWidth += getRequiredWidth(nextNode, layer + 1) - } - - node.isLeaf = requiredWidth === 0 - - node.width = requiredWidth > 0 ? requiredWidth : widthUnit - - return node.width - } - - getRequiredWidth(rootNode, 0) - - // calculate nodes' position - rootNode.x = baseX || 0 - rootNode.y = baseY || 0 - let leafY = rootNode.x - heightUnit * maxLayer - treeBfs(rootNode, (c) => { - let tmpY = c.y - c.width / 2 - for (let nextNode of c.nextNodes) { - nextNode.x = nextNode.isLeaf ? leafY : c.x - heightUnit - nextNode.y = tmpY + nextNode.width / 2 - tmpY += nextNode.width - } - }) - - // calculate box size - let minX = Infinity - let maxX = -Infinity - let minY = Infinity - let maxY = -Infinity - treeBfs(rootNode, (node) => { - if (node.x > maxX) { - maxX = node.x - } - if (node.x < minX) { - minX = node.x - } - if (node.y > maxY) { - maxY = node.y - } - if (node.y < minY) { - minY = node.y - } - }) - let boxWidth = maxX - minX - let boxHeight = maxY - minY - return [boxWidth + actorBoxPadding * 2, boxHeight + actorBoxPadding * 2] - } - - /** - * @param {{ - * g: Group, - * rootNode: {id: any, nextNodes: []}, - * nodeColor: string, - * strokeColor?: string, - * baseX?: number, - * baseY?: number - * }} props - * @param {Group} props.g The group element in canvas engine - * @param {{id: any, nextNodes: []}} props.rootNode The root node of the tree in the actor - * @param {string} props.nodeColor [optional] The filled color of nodes. - * @param {string} props.strokeColor [optional] The color of the stroke. - * @param {number} props.baseX [optional] The x coordination of the lef-top corner. default: 0 - * @param {number} props.baseY [optional] The y coordination of the lef-top corner. default: 0 - * @returns {Group} The group element of this tree - */ - drawActorBox(props) { - if (props.g === undefined) { - throw Error("Invalid Argument: Target group cannot be undefined.") - } - - const actor = props.actor - const group = props.g.append("g") - const rootNode = props.rootNode || [] - const baseX = props.x === undefined ? 0 : props.x - const baseY = props.y === undefined ? 0 : props.y - const strokeColor = props.strokeColor || "white" - const linkColor = props.linkColor || "gray" - - group.attr("class", actor.computeNodeAddress) - - const [boxWidth, boxHeight] = this.calculateActorBoxSize(rootNode) - this.layoutActorBox( - rootNode, - baseX + boxWidth - actorBoxPadding, - baseY + boxHeight / 2 - ) - - const onNodeClicked = (e, node, actor) => { - this.onNodeClick && this.onNodeClick(e, node, actor) - } - - const onActorClick = (e, actor) => { - this.onActorClick && this.onActorClick(e, actor) - } - - /** - * @param {Group} g actor box group - * @param {number} x top-right corner of the label - * @param {number} y top-right corner of the label - * @param {Array} actorIds - * @param {string} color - * @returns {number} width of this label - */ - const drawActorIdLabel = (g, x, y, actorIds, color) => { - y = y - actorBoxStroke - let actorStr = actorIds.toString() - let padding = 15 - // let height = fontSize + 2 * padding; - let gap = 30 - // let polygon = g.append("polygon"); - let textEle = g - .append("text")(actorStr) - .attr("font-size", fontSize) - .position(x - padding - 5, y + padding) - let width = textEle.getWidth() + 2 * padding - // polygon.attr("points", `${x},${y} ${x - width - gap},${y}, ${x - width},${y + height}, ${x},${y + height}`) - // .attr("fill", color); - return width + gap - } - - // draw box - group.attr("id", "actor-" + actor.actorId) - let actorRect = group.append("rect") - for (let representedActor of actor.representedActorList) { - actorRect.classed("actor-" + representedActor.actorId, true) - } - actorRect.classed("fragment-" + actor.fragmentId, true) - actorRect - .init(baseX, baseY, boxWidth, boxHeight) - .attr("fill", this._actorBoxBackgroundColor(actor)) - .attr("rx", actorBoxRadius) - .attr("stroke-width", actorBoxStroke) - .on("click", (e) => onActorClick(e, actor)) - - group - .append("text")(`Fragment ${actor.fragmentId}`) - .position(baseX, baseY - actorBoxStroke - fontSize) - .attr("font-size", fontSize) - - // draw compute node label - let computeNodeToActorIds = new Map() - for (let representedActor of actor.representedActorList) { - if (computeNodeToActorIds.has(representedActor.computeNodeAddress)) { - computeNodeToActorIds - .get(representedActor.computeNodeAddress) - .push(representedActor.actorId) - } else { - computeNodeToActorIds.set(representedActor.computeNodeAddress, [ - representedActor.actorId, - ]) - } - } - let labelStartX = baseX + actorBoxStroke - for (let [addr, actorIds] of computeNodeToActorIds.entries()) { - let w = drawActorIdLabel( - group, - labelStartX, - baseY + boxHeight, - actorIds, - color.TwoGradient(hashIpv4Index(addr))[1] - ) - labelStartX -= w - } - - // draw links - const linkData = [] - treeBfs(rootNode, (c) => { - for (let nextNode of c.nextNodes) { - linkData.push({ - sourceNode: c, - nextNode: nextNode, - source: [c.x, c.y], - target: [nextNode.x, nextNode.y], - }) - } - }) - const linkGen = d3.linkHorizontal() - for (let link of linkData) { - group - .append("path")(linkGen(link)) - .attr( - "stroke-dasharray", - `${internalLinkStrokeWidth / 2},${internalLinkStrokeWidth / 2}` - ) - // .attr("d", linkGen(link)) - .attr("fill", "none") - .attr("class", "actor-" + actor.actorId) - .classed("internal-link", true) - .attr("id", constructInternalLinkId(link.sourceNode, link.nextNode)) - .style("stroke-width", internalLinkStrokeWidth) - .attr("stroke", linkColor) - } - - // draw nodes - treeBfs(rootNode, (node) => { - node.d3Selection = group - .append("circle") - .init(node.x, node.y, operatorNodeRadius) - .attr("id", constructOperatorNodeId(node)) - .attr("stroke", strokeColor) - .attr("fill", this._operatorColor(actor, node)) - .style("cursor", "pointer") - .style("stroke-width", operatorNodeStrokeWidth) - .on("click", (e) => onNodeClicked(e, node, actor)) - group - .append("text")(node.type ? node.type : node.dispatcherType) - .position(node.x, node.y + operatorNodeRadius + 10) - .attr("font-size", fontSize) - }) - - return { - g: group, - x: baseX - boxWidth - actorBoxPadding, - y: baseY - boxHeight / 2 - actorBoxPadding, - width: boxWidth + actorBoxPadding * 2, - height: boxHeight + actorBoxPadding * 2, - } - } - /** - * - * @param {{ - * g: Group, - * actorDagList: Array, - * baseX?: number, - * baseY?: number - * }} props - * @param {Group} props.g The target group contains this group. - * @param {Array} props.actorDagList A list of dag nodes constructed from actors - * { id: actor.actorId, nextNodes: [], actor: actor } - * @param {number} props.baseX [optional] The x coordination of left-top corner. default: 0. - * @param {number} props.baseY [optional] The y coordination of left-top corner. default: 0. - * @returns {{group: Group, width: number, height: number}} The size of the flow - */ - drawFlow(props) { - if (props.g === undefined) { - throw Error("Invalid Argument: Target group cannot be undefined.") - } - - const g = props.g - const actorDagList = props.actorDagList || [] - const baseX = props.baseX || 0 - const baseY = props.baseY || 0 - - let layoutPositionMapper = this.dagLayout(actorDagList) - const actors = [] - for (let actorDag of actorDagList) { - actors.push(actorDag.actor) - } - - // calculate actor box size - for (let actor of actors) { - ;[actor.boxWidth, actor.boxHeight] = this.calculateActorBoxSize( - actor.rootNode - ) - ;[actor.layer, actor.row] = layoutPositionMapper.get(actor.actorId) - } - - // calculate the minimum required width of each layer and row - let maxRow = 0 - let maxLayer = 0 - for (let actor of actors) { - maxLayer = max([actor.layer, maxLayer]) - maxRow = max([actor.row, maxRow]) - } - let rowGap = newNumberArray(maxRow + 1) - let layerGap = newNumberArray(maxLayer + 1) - for (let actor of actors) { - layerGap[actor.layer] = max([layerGap[actor.layer], actor.boxWidth]) - rowGap[actor.row] = max([rowGap[actor.row], actor.boxHeight]) - } - let row2y = newNumberArray(maxRow + 1) - let layer2x = newNumberArray(maxLayer + 1) - row2y = row2y.map((_, r) => { - if (r === 0) { - return 0 - } - let rtn = 0 - for (let i = 0; i < r; ++i) { - rtn += rowGap[i] + gapBetweenRow - } - return rtn - }) - layer2x = layer2x.map((_, l) => { - if (l === 0) { - return 0 - } - let rtn = 0 - for (let i = 0; i < l; ++i) { - rtn += layerGap[i] + gapBetweenLayer - } - return rtn - }) - - // Draw fragment (represent by one actor) - const group = g.append("g") - const linkLayerBackground = group.append("g") - const linkLayer = group.append("g") - const fragmentLayer = group.append("g") - linkLayerBackground.attr("class", "linkLayerBackground") - linkLayer.attr("class", "linkLayer") - fragmentLayer.attr("class", "fragmentLayer") - - let actorBoxList = [] - for (let actor of actors) { - let actorBox = this.drawActorBox({ - actor: actor, - g: fragmentLayer, - rootNode: actor.rootNode, - x: baseX + layer2x[actor.layer], - y: baseY + row2y[actor.row], - strokeColor: "white", - linkColor: "white", - }) - actorBoxList.push(actorBox) - } - - // Draw link between (represent by one actor) - const getLinkBetweenPathStr = (start, end, compensation) => { - const lineGen = d3.line().curve(d3.curveBasis) - let pathStr = lineGen([ - end, - [ - start[0] + - compensation + - actorBoxPadding + - connectionGap + - bendGap * 2, - end[1], - ], - [ - start[0] + compensation + actorBoxPadding + connectionGap + bendGap, - end[1], - ], - [ - start[0] + compensation + actorBoxPadding + connectionGap + bendGap, - start[1], - ], - [start[0] + compensation + actorBoxPadding + connectionGap, start[1]], - start, - ]) - return pathStr - } - - let linkData = [] - for (let actor of actors) { - for (let outputNode of actor.output) { - linkData.push({ - actor: actor, - d: getLinkBetweenPathStr( - [actor.rootNode.x, actor.rootNode.y], - [outputNode.x, outputNode.y], - layerGap[actor.layer] - actor.boxWidth - ), - }) - } - } - - for (let s of linkData) { - linkLayer - .append("path")(s.d) - .attr( - "stroke-dasharray", - `${outgoingLinkStrokeWidth},${outgoingLinkStrokeWidth}` - ) - .attr("fill", "none") - .attr("class", "actor-" + s.actor.actorId) - .classed("outgoing-link", true) - .style("stroke-width", outgoingLinkStrokeWidth) - .attr("stroke", this._actorOutgoinglinkColor(s.actor)) - .attr("layer", "back") - } - - for (let s of linkData) { - linkLayerBackground - .append("path")(s.d) - .attr("fill", "none") - .style("stroke-width", outgoingLinkBgStrokeWidth) - .attr("class", "actor-" + s.actor.actorId) - .classed("outgoing-link-bg", true) - .attr("stroke", outGoingLinkBgColor) - .attr("layer", "back") - } - - // calculate box size - let width = 0 - let height = 0 - for (let actorBox of actorBoxList) { - let biggestX = actorBox.x - baseX + actorBox.width - let biggestY = actorBox.y - baseY + actorBox.height - width = max([biggestX, width]) - height = max([biggestY, height]) - } - - group.attr("class", "flowchart") - return { - g: group, - width: width, - height: height, - } - } - - /** - * A flow is an extracted connected component of actors of - * the raw response from the meta node. This method will first - * merge actors in the same fragment using some identifier - * (currently it is the id of the operator before the dispatcher). - * And then use `drawFlow()` to draw each connected component. - */ - drawManyFlow() { - const g = this.topGroup - const baseX = 0 - const baseY = 0 - - g.attr("id", "") - - let fragmentRepresentedActors = this.streamPlan.fragmentRepresentedActors - // get dag layout of these actors - let dagNodeMap = new Map() - for (let actor of fragmentRepresentedActors) { - actor.rootNode.actorId = actor.actorId - treeBfs(actor.rootNode, (node) => { - node.actorId = actor.actorId - }) - dagNodeMap.set(actor.actorId, { - id: actor.actorId, - nextNodes: [], - actor: actor, - }) - } - for (let actor of fragmentRepresentedActors) { - for (let outputActorNode of actor.output) { - let outputDagNode = dagNodeMap.get(outputActorNode.actorId) - if (outputDagNode) { - // the output actor node is in a represented actor - dagNodeMap.get(actor.actorId).nextNodes.push(outputDagNode) - } - } - } - let actorDagNodes = [] - for (let id of dagNodeMap.keys()) { - actorDagNodes.push(dagNodeMap.get(id)) - } - - let actorsList = getConnectedComponent(actorDagNodes) - - let y = baseY - for (let actorDagList of actorsList) { - let flowChart = this.drawFlow({ - g: g, - baseX: baseX, - baseY: y, - actorDagList: actorDagList, - }) - y += flowChart.height + gapBetweenFlowChart - } - } -} - -/** - * create a graph view based on raw input from the meta node, - * and append the svg component to the giving svg group. - * @param {Group} g The parent group contain the graph. - * @param {any} data Raw response from the meta node. e.g. [{node: {...}, actors: {...}}, ...] - * @param {(clickEvent, node, actor) => void} onNodeClick callback when a node (operator) is clicked. - * @param {{type: string, node: {host: {host: string, port: number}}, id?: number}} selectedWokerNode - * @returns {StreamChartHelper} - */ -export default function createView( - engine, - data, - onNodeClick, - onActorClick, - selectedWokerNode, - shownActorIdList -) { - console.log(shownActorIdList, "shownActorList") - let streamChartHelper = new StreamChartHelper( - engine.topGroup, - data, - onNodeClick, - onActorClick, - selectedWokerNode, - shownActorIdList - ) - streamChartHelper.drawManyFlow() - return streamChartHelper -} diff --git a/dashboard/test/algo.test.js b/dashboard/test/algo.test.js deleted file mode 100644 index 9fe47669fed6e..0000000000000 --- a/dashboard/test/algo.test.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2024 RisingWave Labs - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -import { Node } from "../lib/algo" -import { StreamChartHelper } from "../lib/streamPlan/streamChartHelper" - -describe("Algo", () => { - it("should generate right dag layout", () => { - // fake data - let nodes = [] - for (let i = 0; i < 10; ++i) { - nodes.push(new Node([], i + 1)) - } - const n = (i) => nodes[i - 1] - n(1).nextNodes = [n(2), n(3)] - n(2).nextNodes = [n(9)] - n(3).nextNodes = [n(5), n(10)] - n(4).nextNodes = [n(5)] - n(5).nextNodes = [n(6), n(7)] - n(6).nextNodes = [n(9), n(10)] - n(7).nextNodes = [n(8)] - - let dagPositionMapper = new StreamChartHelper().dagLayout(nodes) - - // construct map - let maxLayer = 0 - let maxRow = 0 - for (let node of dagPositionMapper.keys()) { - let pos = dagPositionMapper.get(node) - maxLayer = pos[0] > maxLayer ? pos[0] : maxLayer - maxRow = pos[1] > maxRow ? pos[1] : maxRow - } - let m = [] - for (let i = 0; i < maxLayer + 1; ++i) { - m.push([]) - for (let r = 0; r < maxRow + 1; ++r) { - m[i].push([]) - } - } - for (let node of dagPositionMapper.keys()) { - let pos = dagPositionMapper.get(node) - m[pos[0]][pos[1]] = node - } - - // search - const _search = (l, r, d) => { - // Layer, Row - if (l > maxLayer || r > maxRow || r < 0) { - return false - } - if (m[l][r].id !== undefined) { - return m[l][r].id === d - } - return _search(l + 1, r, d) - } - - const canReach = (node, nextNode) => { - let pos = dagPositionMapper.get(node) - for (let r = 0; r <= maxRow; ++r) { - if (_search(pos[0] + 1, r, nextNode.id)) { - return true - } - } - return false - } - - //check all links - let ok = true - for (let node of nodes) { - for (let nextNode of node.nextNodes) { - if (!canReach(node, nextNode)) { - console.error( - `Failed to connect node ${node.id} to node ${nextNode.id}` - ) - ok = false - break - } - } - if (!ok) { - break - } - } - - // visualization - // let s = ""; - // for(let r = maxRow; r >= 0; --r){ - // for(let l = 0; l <= maxLayer; ++l){ - // s += `\t${m[l][r].id ? m[l][r].id : " "}` - // } - // s += "\n" - // } - // console.log(s); - - expect(ok).toEqual(true) - }) -})