diff --git a/package-lock.json b/package-lock.json index 8611bfbe..1aba29fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@spcl/sdfv", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@spcl/sdfv", - "version": "1.1.0", + "version": "1.1.1", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -23,6 +23,7 @@ "material-icons": "^1.13.1", "material-symbols": "^0.6.0", "mathjs": "^11.5.0", + "monaco-editor": "^0.44.0", "patch-package": "^6.5.1", "pixi-viewport": "4.24", "pixi.js": "6.1.3", @@ -9745,6 +9746,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/monaco-editor": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.44.0.tgz", + "integrity": "sha512-5SmjNStN6bSuSE5WPT2ZV+iYn1/yI9sd4Igtk23ChvqB7kDk9lZbB9F5frsuvpB+2njdIeGGFf2G4gbE6rCC9Q==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 4c7a7df2..c9405b88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@spcl/sdfv", - "version": "1.1.0", + "version": "1.1.1", "description": "A standalone viewer for SDFGs", "homepage": "https://github.com/spcl/dace-webclient", "main": "out/index.js", @@ -78,6 +78,7 @@ "material-icons": "^1.13.1", "material-symbols": "^0.6.0", "mathjs": "^11.5.0", + "monaco-editor": "^0.44.0", "patch-package": "^6.5.1", "pixi-viewport": "4.24", "pixi.js": "6.1.3", diff --git a/sdfv.css b/sdfv.css index 260f8a00..021f2042 100644 --- a/sdfv.css +++ b/sdfv.css @@ -383,6 +383,12 @@ pre.code code { --color-selected-highlighted: darkorange; --color-minimap-viewport: #ff7a00; + --tasklet-input-color: black; + --tasklet-output-color: black; + --tasklet-symbol-color: red; + --tasklet-number-color: black; + --tasklet-highlighted-color: green; + --overlay-color-lightness: 0.5; --overlay-color-saturation: 1.0; diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index b014db41..2def3559 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -55,6 +55,7 @@ import { SDFGNode, ScopeBlock, State, + Tasklet, drawSDFG, offset_sdfg, offset_state @@ -2543,6 +2544,12 @@ export class SDFGRenderer extends EventEmitter { (type: any, e: any, obj: any) => { obj.hovered = false; obj.highlighted = false; + if (obj instanceof Tasklet) { + for (const t of obj.inputTokens) + t.highlighted = false; + for (const t of obj.outputTokens) + t.highlighted = false; + } } ); // Mark hovered and highlighted elements. @@ -2581,23 +2588,42 @@ export class SDFGRenderer extends EventEmitter { ); } - // Highlight all access nodes with the same name as the hovered - // connector in the nested sdfg - if (intersected && obj instanceof Connector && e.graph) { - const nested_graph = e.graph.node(obj.parent_id).data.graph; - if (nested_graph) { - traverseSDFGScopes(nested_graph, (node: any) => { - // If node is a state, then visit sub-scope - if (node instanceof State) { - return true; + if (intersected && obj instanceof Connector) { + // Highlight all access nodes with the same name as the + // hovered connector in the nested sdfg + if (e.graph) { + const nested_graph = + e.graph.node(obj.parent_id).data.graph; + if (nested_graph) { + traverseSDFGScopes(nested_graph, (node: any) => { + // If node is a state, then visit sub-scope + if (node instanceof State) { + return true; + } + if (node instanceof AccessNode && + node.data.node.label === obj.label()) { + node.highlighted = true; + } + // No need to visit sub-scope + return false; + }); + } + } + + // Similarly, highlight any identifiers in a connector's + // tasklet, if applicable. + if (obj.linkedElem && obj.linkedElem instanceof Tasklet) { + if (obj.connectorType === 'in') { + for (const token of obj.linkedElem.inputTokens) { + if (token.token === obj.data.name) + token.highlighted = true; } - if (node instanceof AccessNode && - node.data.node.label === obj.label()) { - node.highlighted = true; + } else { + for (const token of obj.linkedElem.outputTokens) { + if (token.token === obj.data.name) + token.highlighted = true; } - // No need to visit sub-scope - return false; - }); + } } } @@ -3557,6 +3583,8 @@ function relayoutSDFGState( conns = Object.keys(node.attributes.layout.in_connectors); for (const cname of conns) { const conn = new Connector({ name: cname }, i, sdfg, node.id); + conn.connectorType = 'in'; + conn.linkedElem = obj; obj.in_connectors.push(conn); i += 1; } @@ -3569,6 +3597,8 @@ function relayoutSDFGState( conns = Object.keys(node.attributes.layout.out_connectors); for (const cname of conns) { const conn = new Connector({ name: cname }, i, sdfg, node.id); + conn.connectorType = 'out'; + conn.linkedElem = obj; obj.out_connectors.push(conn); i += 1; } diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index ccade2f2..0be4c07a 100644 --- a/src/renderer/renderer_elements.ts +++ b/src/renderer/renderer_elements.ts @@ -11,6 +11,7 @@ import { SimpleRect } from '../index'; import { SDFV } from '../sdfv'; +import { editor } from 'monaco-editor'; import { sdfg_consume_elem_to_string, sdfg_property_to_string, @@ -935,11 +936,17 @@ export class Memlet extends Edge { renderer.set_tooltip((c) => this.tooltip(c, renderer)); ctx.fillStyle = ctx.strokeStyle = this.strokeStyle(renderer); - // CR edges have dashed lines - if (this.data.attributes.wcr !== null) - ctx.setLineDash([2, 2]); - else - ctx.setLineDash([1, 0]); + let skipArrow = false; + if (this.attributes().data) { + // CR edges have dashed lines + if (this.data.attributes.wcr !== null) + ctx.setLineDash([3, 2]); + else + ctx.setLineDash([1, 0]); + } else { + // Empty memlet, i.e., a dependency edge. Do not draw the arrowhead. + skipArrow = true; + } ctx.stroke(); @@ -957,10 +964,11 @@ export class Memlet extends Edge { ); } - this.drawArrow( - ctx, this.points[this.points.length - 2], - this.points[this.points.length - 1], 3 - ); + if (!skipArrow) + this.drawArrow( + ctx, this.points[this.points.length - 2], + this.points[this.points.length - 1], 3 + ); } public tooltip( @@ -1134,7 +1142,9 @@ export class InterstateEdge extends Edge { return; if ((ctx as any).lod && ppp >= SDFV.SCOPE_LOD) return; - ctx.fillStyle = this.getCssProperty(renderer, '--color-default'); + ctx.fillStyle = this.getCssProperty( + renderer, '--interstate-edge-color' + ); const oldFont = ctx.font; ctx.font = '8px sans-serif'; const labelMetrics = ctx.measureText(this.label()); @@ -1153,7 +1163,10 @@ export class InterstateEdge extends Edge { } export class Connector extends SDFGElement { + public custom_label: string | null = null; + public linkedElem?: SDFGElement; + public connectorType: 'in' | 'out' = 'in'; public draw( renderer: SDFGRenderer, ctx: CanvasRenderingContext2D, @@ -1648,8 +1661,179 @@ export class PipelineExit extends ExitNode { } +enum TaskletCodeTokenType { + Number, + Input, + Output, + Symbol, + Other, +} + +type TaskletCodeToken = { + token: string, + type: TaskletCodeTokenType, + highlighted: boolean, +}; + export class Tasklet extends SDFGNode { + public constructor( + public data: any, + public id: number, + public sdfg: JsonSDFG, + public parent_id: number | null = null, + public parentElem?: SDFGElement, + ) { + super(data, id, sdfg, parent_id, parentElem); + this.highlightCode(); + } + + private highlightedCode: TaskletCodeToken[][] = []; + public readonly inputTokens: Set = new Set(); + public readonly outputTokens: Set = new Set(); + private longestCodeLine?: string; + + public async highlightCode(): Promise { + this.inputTokens.clear(); + this.outputTokens.clear(); + this.highlightedCode = []; + + const lang = this.attributes().code.language?.toLowerCase() || 'python'; + const code = this.attributes().code.string_data; + + const sdfgSymbols = Object.keys(this.sdfg.attributes.symbols); + const inConnectors = Object.keys(this.attributes().in_connectors); + const outConnectors = Object.keys(this.attributes().out_connectors); + + const lines = code.split('\n'); + let maxline_len = 0; + for (const line of lines) { + if (line.length > maxline_len) { + this.longestCodeLine = line; + maxline_len = line.length; + } + + const highlightedLine: TaskletCodeToken[] = []; + const tokens = editor.tokenize(line, lang)[0]; + if (tokens.length < 2) { + highlightedLine.push({ + token: line, + type: TaskletCodeTokenType.Other, + highlighted: false, + }); + } else { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + const next = i + 1 < tokens.length ? tokens[i + 1] : null; + const endPos = next?.offset; + const tokenValue = line.substring(token.offset, endPos); + + const taskletToken: TaskletCodeToken = { + token: tokenValue, + type: TaskletCodeTokenType.Other, + highlighted: false, + }; + if (token.type.startsWith('identifier')) { + if (sdfgSymbols.includes(tokenValue)) { + taskletToken.type = TaskletCodeTokenType.Symbol; + } else if (inConnectors.includes(tokenValue)) { + taskletToken.type = TaskletCodeTokenType.Input; + this.inputTokens.add(taskletToken); + } else if (outConnectors.includes(tokenValue)) { + taskletToken.type = TaskletCodeTokenType.Output; + this.outputTokens.add(taskletToken); + } + } else if (token.type.startsWith('number')) { + taskletToken.type = TaskletCodeTokenType.Number; + } + + highlightedLine.push(taskletToken); + } + } + this.highlightedCode.push(highlightedLine); + } + } + + private drawTaskletCode( + renderer: SDFGRenderer, ctx: CanvasRenderingContext2D + ): void { + if (!this.longestCodeLine) + return; + + const oldfont = ctx.font; + ctx.font = '10px courier new'; + const textmetrics = ctx.measureText(this.longestCodeLine); + + // Fit font size to 80% height and width of tasklet + const height = this.highlightedCode.length * SDFV.LINEHEIGHT * 1.05; + const width = textmetrics.width; + const TASKLET_WRATIO = 0.9, TASKLET_HRATIO = 0.5; + const hr = height / (this.height * TASKLET_HRATIO); + const wr = width / (this.width * TASKLET_WRATIO); + const fontSize = Math.min(10 / hr, 10 / wr); + const textYOffset = fontSize / 4; + + ctx.font = fontSize + 'px courier new'; + const defaultColor = this.getCssProperty( + renderer, '--node-foreground-color' + ); + // Set the start offset such that the middle row of the text is in + // this.y + const startY = this.y + textYOffset - ( + (this.highlightedCode.length - 1) / 2 + ) * fontSize * 1.05; + const startX = this.x - (this.width * TASKLET_WRATIO) / 2.0; + let i = 0; + for (const line of this.highlightedCode) { + const lineY = startY + i * fontSize * 1.05; + let tokenX = startX; + for (const token of line) { + const ofont = ctx.font; + if (token.highlighted) { + ctx.font = 'bold ' + fontSize + 'px courier new'; + ctx.fillStyle = this.getCssProperty( + renderer, '--tasklet-highlighted-color' + ); + } else { + switch (token.type) { + case TaskletCodeTokenType.Input: + ctx.fillStyle = this.getCssProperty( + renderer, '--tasklet-input-color' + ); + break; + case TaskletCodeTokenType.Output: + ctx.fillStyle = this.getCssProperty( + renderer, '--tasklet-output-color' + ); + break; + case TaskletCodeTokenType.Symbol: + ctx.font = 'bold ' + fontSize + 'px courier new'; + ctx.fillStyle = this.getCssProperty( + renderer, '--tasklet-symbol-color' + ); + break; + case TaskletCodeTokenType.Number: + ctx.fillStyle = this.getCssProperty( + renderer, '--tasklet-number-color' + ); + break; + default: + ctx.fillStyle = defaultColor; + break; + } + } + + ctx.fillText(token.token, tokenX, lineY); + const tokenWidth = ctx.measureText(token.token).width; + tokenX += tokenWidth; + ctx.font = ofont; + } + i++; + } + + ctx.font = oldfont; + } + public draw( renderer: SDFGRenderer, ctx: CanvasRenderingContext2D, _mousepos?: Point2D @@ -1677,49 +1861,14 @@ export class Tasklet extends SDFGNode { const ppp = canvas_manager.points_per_pixel(); if (!(ctx as any).lod || ppp < SDFV.TASKLET_LOD) { // If we are close to the tasklet, show its contents - const code = this.attributes().code.string_data; - const lines = code.split('\n'); - let maxline = 0, maxline_len = 0; - for (let i = 0; i < lines.length; i++) { - if (lines[i].length > maxline_len) { - maxline = i; - maxline_len = lines[i].length; - } - } - const oldfont = ctx.font; - ctx.font = '10px courier new'; - const textmetrics = ctx.measureText(lines[maxline]); - - // Fit font size to 80% height and width of tasklet - const height = lines.length * SDFV.LINEHEIGHT * 1.05; - const width = textmetrics.width; - const TASKLET_WRATIO = 0.9, TASKLET_HRATIO = 0.5; - const hr = height / (this.height * TASKLET_HRATIO); - const wr = width / (this.width * TASKLET_WRATIO); - const FONTSIZE = Math.min(10 / hr, 10 / wr); - const text_yoffset = FONTSIZE / 4; - - ctx.font = FONTSIZE + 'px courier new'; - // Set the start offset such that the middle row of the text is in - // this.y - const y = this.y + text_yoffset - ( - (lines.length - 1) / 2 - ) * FONTSIZE * 1.05; - for (let i = 0; i < lines.length; i++) - ctx.fillText( - lines[i], this.x - (this.width * TASKLET_WRATIO) / 2.0, - y + i * FONTSIZE * 1.05 - ); - - ctx.font = oldfont; - return; + this.drawTaskletCode(renderer, ctx); + } else { + const textmetrics = ctx.measureText(this.label()); + ctx.fillText( + this.label(), this.x - textmetrics.width / 2.0, + this.y + SDFV.LINEHEIGHT / 2.0 + ); } - - const textmetrics = ctx.measureText(this.label()); - ctx.fillText( - this.label(), this.x - textmetrics.width / 2.0, - this.y + SDFV.LINEHEIGHT / 2.0 - ); } public shade( @@ -2033,11 +2182,13 @@ function batchedDrawEdges( )) return; - // WCR Edge. if (!(graph instanceof State)) { - if (edge.parent_id !== null && edge.data.attributes.wcr !== null) { - deferredEdges.push(edge); - return; + if (edge.parent_id !== null) { + // WCR edge or dependency edge. + if (edge.attributes().wcr !== null || !edge.attributes().data) { + deferredEdges.push(edge); + return; + } } }