From a4588fd1a3183b5e248f6727569274c27f6ae5d1 Mon Sep 17 00:00:00 2001 From: Leandro Treu Date: Sat, 4 May 2024 00:12:30 +0200 Subject: [PATCH] Edges and Overlays Optimizations (#141) * graph startup collapsed, group overlay bug, collapsed NestedSDFG lod change * added missing comment * added code to layouter to order the connectors (much less edges intertwine) * first code for summarised edges, adjusted nodesep & ranksep * removed edge summary for tasklets, added summary symbol * changed edge summary symbol, hovering nodes and connectors shows summarised edges * fixed error when selecting memlet * fixed fill_info to show memlet src/dst connectors * fixed summary symbol toggled on when not applicable * added shift+rightclick to expand, ctrl+scroll to verticalscroll * memory overlays LOD threshold fixes * overlays state LOD changes * renderer loop region bugfix and overlay comment * fixed LogicalGroupOverlay on by default but UI not updated, optimized code if logical groups array has length 0 * AvgParallelismOverlay reworked lod code based on new renderer * STATE_LOD comment, changed all LOD threshold checks to strict inequalities * DepthOverlay lod rework * OpIntOverlay lod rework * Overlays State lod rework, removed debug * changed spelling to summarize * moved Dagre layout options to sdfv.ts * removed commented out section, formatting adjusted --- src/overlays/avg_parallelism_overlay.ts | 22 +- src/overlays/depth_overlay.ts | 22 +- src/overlays/logical_group_overlay.ts | 13 +- src/overlays/memory_location_overlay.ts | 10 +- src/overlays/memory_volume_overlay.ts | 23 +- src/overlays/operational_intensity_overlay.ts | 22 +- src/overlays/runtime_micro_seconds_overlay.ts | 22 +- ...simulated_operational_intensity_overlay.ts | 22 +- src/overlays/static_flops_overlay.ts | 22 +- src/renderer/renderer.ts | 313 +++++++++++++++--- src/renderer/renderer_elements.ts | 168 +++++++++- src/sdfv.ts | 24 +- 12 files changed, 530 insertions(+), 153 deletions(-) diff --git a/src/overlays/avg_parallelism_overlay.ts b/src/overlays/avg_parallelism_overlay.ts index 84b95567..6edc264e 100644 --- a/src/overlays/avg_parallelism_overlay.ts +++ b/src/overlays/avg_parallelism_overlay.ts @@ -230,8 +230,8 @@ export class AvgParallelismOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - block.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(block.width * block.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || block.attributes()?.is_collapsed) { this.shadeNode(block, ctx); } else if (block instanceof State) { @@ -245,20 +245,20 @@ export class AvgParallelismOverlay extends GenericSdfgOverlay { visibleRect.y, visibleRect.w, visibleRect.h)) return; - if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { - this.shadeNode(node, ctx); - } else { - if (node instanceof NestedSDFG && - node.attributes().sdfg && - node.attributes().sdfg.type !== 'SDFGShell') { + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if ((ctx as any).lod && nodeppp < SDFV.STATE_LOD) { + this.shadeNode(node, ctx); + } + else if (node.attributes().sdfg && node.attributes().sdfg.type !== 'SDFGShell') { this.recursivelyShadeCFG( node.data.graph, ctx, ppp, visibleRect ); - } else { - this.shadeNode(node, ctx); } } + else { + this.shadeNode(node, ctx); + } }); } } else if (block instanceof ControlFlowRegion) { diff --git a/src/overlays/depth_overlay.ts b/src/overlays/depth_overlay.ts index 955d6046..52b3277f 100644 --- a/src/overlays/depth_overlay.ts +++ b/src/overlays/depth_overlay.ts @@ -215,8 +215,8 @@ export class DepthOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - state.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(state.width * state.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || state.data.state.attributes.is_collapsed) { this.shade_node(state, ctx); } else { @@ -230,20 +230,20 @@ export class DepthOverlay extends GenericSdfgOverlay { visible_rect.y, visible_rect.w, visible_rect.h)) return; - if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { - this.shade_node(node, ctx); - } else { - if (node instanceof NestedSDFG && - node.attributes().sdfg && - node.attributes().sdfg.type !== 'SDFGShell') { + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if ((ctx as any).lod && nodeppp < SDFV.STATE_LOD) { + this.shade_node(node, ctx); + } + else if (node.attributes().sdfg && node.attributes().sdfg.type !== 'SDFGShell') { this.recursively_shade_sdfg( node.data.graph, ctx, ppp, visible_rect ); - } else { - this.shade_node(node, ctx); } } + else { + this.shade_node(node, ctx); + } }); } } diff --git a/src/overlays/logical_group_overlay.ts b/src/overlays/logical_group_overlay.ts index 90badcc9..6acc5064 100644 --- a/src/overlays/logical_group_overlay.ts +++ b/src/overlays/logical_group_overlay.ts @@ -88,8 +88,13 @@ export class LogicalGroupOverlay extends GenericSdfgOverlay { // In that case, we overlay the correct grouping color(s). // If it's expanded or zoomed in close enough, we traverse inside. const sdfgGroups = sdfg.attributes.logical_groups; - if (sdfgGroups === undefined) + if (sdfgGroups === undefined || sdfgGroups.length === 0) { return; + } + + if (!graph) { + return; + } graph?.nodes().forEach(v => { const block = graph.node(v); @@ -101,8 +106,8 @@ export class LogicalGroupOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - block.width / ppp <= SDFV.STATE_LOD)) || + const blockppp = Math.sqrt(block.width * block.height) / ppp; + if (((ctx as any).lod && (blockppp < SDFV.STATE_LOD)) || block.attributes().is_collapsed ) { this.shadeNode(block, sdfgGroups, ctx); @@ -121,7 +126,7 @@ export class LogicalGroupOverlay extends GenericSdfgOverlay { return; if (node.attributes().is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { + ((ctx as any).lod && ppp > SDFV.NODE_LOD)) { this.shadeNode(node, sdfgGroups, ctx); } else { if (node instanceof NestedSDFG && diff --git a/src/overlays/memory_location_overlay.ts b/src/overlays/memory_location_overlay.ts index 5cb2f23c..914ba3c0 100644 --- a/src/overlays/memory_location_overlay.ts +++ b/src/overlays/memory_location_overlay.ts @@ -243,10 +243,10 @@ export class MemoryLocationOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - block.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(block.width * block.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || block.attributes()?.is_collapsed) { - // The block is collapsed or invisible, so we don't need to + // The state is collapsed or too small, so we don't need to // traverse its insides. return; } else if (block instanceof State) { @@ -267,7 +267,9 @@ export class MemoryLocationOverlay extends GenericSdfgOverlay { node.data.graph, ctx, ppp, visibleRect ); } else if (node instanceof AccessNode) { - this.shadeNode(node, ctx); + if (!(ctx as any).lod || ppp < SDFV.NODE_LOD) { + this.shadeNode(node, ctx); + } } }); } diff --git a/src/overlays/memory_volume_overlay.ts b/src/overlays/memory_volume_overlay.ts index faf8e863..cebec124 100644 --- a/src/overlays/memory_volume_overlay.ts +++ b/src/overlays/memory_volume_overlay.ts @@ -173,13 +173,6 @@ export class MemoryVolumeOverlay extends GenericSdfgOverlay { graph.nodes().forEach(v => { const block: ControlFlowBlock = graph.node(v); - // If we're zoomed out enough that the contents aren't visible, we - // skip the state. - if ((ctx as any).lod && ( - ppp >= SDFV.STATE_LOD || block.width / ppp < SDFV.STATE_LOD - )) - return; - // If the node's invisible, we skip it. if ((ctx as any).lod && !block.intersect( visibleRect.x, visibleRect.y, @@ -187,6 +180,12 @@ export class MemoryVolumeOverlay extends GenericSdfgOverlay { ) || block.attributes()?.is_collapsed) return; + // If we're zoomed out enough that the contents aren't visible, we + // skip the state. + const stateppp = Math.sqrt(block.width * block.height) / ppp; + if ((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) + return; + if (block instanceof State) { const state_graph = block.data.graph; if (state_graph) { @@ -203,7 +202,7 @@ export class MemoryVolumeOverlay extends GenericSdfgOverlay { // If we're zoomed out enough that the node's contents // aren't visible or the node is collapsed, we skip it. if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) + ((ctx as any).lod && ppp > SDFV.NODE_LOD)) return; if (node instanceof NestedSDFG && @@ -218,8 +217,12 @@ export class MemoryVolumeOverlay extends GenericSdfgOverlay { state_graph.edges().forEach((e: any) => { const edge: Edge = state_graph.edge(e); - if ((ctx as any).lod && !edge.intersect(visibleRect.x, - visibleRect.y, visibleRect.w, visibleRect.h)) + // Skip if edge is invisible, or zoomed out far + if ((ctx as any).lod + && (!edge.intersect(visibleRect.x, visibleRect.y, visibleRect.w, visibleRect.h) + || ppp > SDFV.EDGE_LOD + ) + ) return; this.shadeEdge(edge, ctx); diff --git a/src/overlays/operational_intensity_overlay.ts b/src/overlays/operational_intensity_overlay.ts index 6cd7d4f3..a98b5422 100644 --- a/src/overlays/operational_intensity_overlay.ts +++ b/src/overlays/operational_intensity_overlay.ts @@ -258,8 +258,8 @@ export class OperationalIntensityOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - state.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(state.width * state.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || state.data.state.attributes.is_collapsed) { this.shade_node(state, ctx); } else { @@ -273,20 +273,20 @@ export class OperationalIntensityOverlay extends GenericSdfgOverlay { visible_rect.y, visible_rect.w, visible_rect.h)) return; - if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { - this.shade_node(node, ctx); - } else { - if (node instanceof NestedSDFG && - node.attributes().sdfg && - node.attributes().sdfg.type !== 'SDFGShell') { + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if ((ctx as any).lod && nodeppp < SDFV.STATE_LOD) { + this.shade_node(node, ctx); + } + else if (node.attributes().sdfg && node.attributes().sdfg.type !== 'SDFGShell') { this.recursively_shade_sdfg( node.data.graph, ctx, ppp, visible_rect ); - } else { - this.shade_node(node, ctx); } } + else { + this.shade_node(node, ctx); + } }); } } diff --git a/src/overlays/runtime_micro_seconds_overlay.ts b/src/overlays/runtime_micro_seconds_overlay.ts index bc22790b..b68c911f 100644 --- a/src/overlays/runtime_micro_seconds_overlay.ts +++ b/src/overlays/runtime_micro_seconds_overlay.ts @@ -131,8 +131,8 @@ export class RuntimeMicroSecondsOverlay extends RuntimeReportOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - state.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(state.width * state.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || state.data.state.attributes.is_collapsed) { this.shade_node(state, ctx); } else { @@ -146,20 +146,20 @@ export class RuntimeMicroSecondsOverlay extends RuntimeReportOverlay { visible_rect.y, visible_rect.w, visible_rect.h)) return; - if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { - this.shade_node(node, ctx); - } else { - if (node instanceof NestedSDFG && - node.attributes().sdfg && - node.attributes().sdfg.type !== 'SDFGShell') { + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if ((ctx as any).lod && nodeppp < SDFV.STATE_LOD) { + this.shade_node(node, ctx); + } + else if (node.attributes().sdfg && node.attributes().sdfg.type !== 'SDFGShell') { this.recursively_shade_sdfg( node.data.graph, ctx, ppp, visible_rect ); - } else { - this.shade_node(node, ctx); } } + else { + this.shade_node(node, ctx); + } }); } } diff --git a/src/overlays/simulated_operational_intensity_overlay.ts b/src/overlays/simulated_operational_intensity_overlay.ts index b73d4301..529c68ff 100644 --- a/src/overlays/simulated_operational_intensity_overlay.ts +++ b/src/overlays/simulated_operational_intensity_overlay.ts @@ -221,8 +221,8 @@ export class SimulatedOperationalIntensityOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - state.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(state.width * state.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || state.data.state.attributes.is_collapsed) { this.shade_node(state, ctx); } else { @@ -236,20 +236,20 @@ export class SimulatedOperationalIntensityOverlay extends GenericSdfgOverlay { visible_rect.y, visible_rect.w, visible_rect.h)) return; - if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { - this.shade_node(node, ctx); - } else { - if (node instanceof NestedSDFG && - node.attributes().sdfg && - node.attributes().sdfg.type !== 'SDFGShell') { + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if ((ctx as any).lod && nodeppp < SDFV.STATE_LOD) { + this.shade_node(node, ctx); + } + else if (node.attributes().sdfg && node.attributes().sdfg.type !== 'SDFGShell') { this.recursively_shade_sdfg( node.data.graph, ctx, ppp, visible_rect ); - } else { - this.shade_node(node, ctx); } } + else { + this.shade_node(node, ctx); + } }); } } diff --git a/src/overlays/static_flops_overlay.ts b/src/overlays/static_flops_overlay.ts index b68326c8..98febc74 100644 --- a/src/overlays/static_flops_overlay.ts +++ b/src/overlays/static_flops_overlay.ts @@ -215,8 +215,8 @@ export class StaticFlopsOverlay extends GenericSdfgOverlay { )) return; - if (((ctx as any).lod && (ppp >= SDFV.STATE_LOD || - state.width / ppp <= SDFV.STATE_LOD)) || + const stateppp = Math.sqrt(state.width * state.height) / ppp; + if (((ctx as any).lod && (stateppp < SDFV.STATE_LOD)) || state.data.state.attributes.is_collapsed) { this.shade_node(state, ctx); } else { @@ -230,20 +230,20 @@ export class StaticFlopsOverlay extends GenericSdfgOverlay { visible_rect.y, visible_rect.w, visible_rect.h)) return; - if (node.data.node.attributes.is_collapsed || - ((ctx as any).lod && ppp >= SDFV.NODE_LOD)) { - this.shade_node(node, ctx); - } else { - if (node instanceof NestedSDFG && - node.attributes().sdfg && - node.attributes().sdfg.type !== 'SDFGShell') { + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if ((ctx as any).lod && nodeppp < SDFV.STATE_LOD) { + this.shade_node(node, ctx); + } + else if (node.attributes().sdfg && node.attributes().sdfg.type !== 'SDFGShell') { this.recursively_shade_sdfg( node.data.graph, ctx, ppp, visible_rect ); - } else { - this.shade_node(node, ctx); } } + else { + this.shade_node(node, ctx); + } }); } } diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 0f023bf7..1c4ff3a8 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -51,6 +51,7 @@ import { ControlFlowBlock, ControlFlowRegion, Edge, EntryNode, InterstateEdge, LoopRegion, Memlet, NestedSDFG, + ScopeNode, SDFG, SDFGElement, SDFGElementType, @@ -262,9 +263,6 @@ export class SDFGRenderer extends EventEmitter { this.overlay_manager = new OverlayManager(this); - // Register overlays that are turned on by default. - this.overlay_manager.register_overlay(LogicalGroupOverlay); - this.in_vscode = false; try { vscode; @@ -596,7 +594,7 @@ export class SDFGRenderer extends EventEmitter { }).appendTo(this.toolbar).append(overlayDropdown); const addOverlayToMenu = ( - txt: string, ol: typeof GenericSdfgOverlay + txt: string, ol: typeof GenericSdfgOverlay, default_state: boolean ) => { const olItem = $('
  • ', { css: { @@ -609,6 +607,7 @@ export class SDFGRenderer extends EventEmitter { const olInput = $('', { class: 'form-check-input', type: 'checkbox', + checked: default_state, change: () => { if (olInput.prop('checked')) this.overlay_manager?.register_overlay(ol); @@ -622,11 +621,13 @@ export class SDFGRenderer extends EventEmitter { }).appendTo(olContainer); }; - addOverlayToMenu('Logical groups', LogicalGroupOverlay); - addOverlayToMenu('Storage locations', MemoryLocationOverlay); - addOverlayToMenu( - 'Logical data movement volume', MemoryVolumeOverlay - ); + // Register overlays that are turned on by default. + this.overlay_manager.register_overlay(LogicalGroupOverlay); + addOverlayToMenu('Logical groups', LogicalGroupOverlay, true); + + // Add overlays that are turned off by default. + addOverlayToMenu('Storage locations', MemoryLocationOverlay, false); + addOverlayToMenu('Logical data movement volume', MemoryVolumeOverlay, false); } // Zoom to fit. @@ -901,10 +902,12 @@ export class SDFGRenderer extends EventEmitter { else this.bgcolor = window.getComputedStyle(this.canvas).backgroundColor; - // Create the initial SDFG layout - // Loading animation already started in the file_read_complete function - // in sdfv.ts to also include the JSON parsing step. + this.updateCFGList(); + + // Create the initial SDFG layout + // Loading animation already started in the file_read_complete function in sdfv.ts + // to also include the JSON parsing step. this.relayout(); // Set mouse event handlers @@ -2674,6 +2677,53 @@ export class SDFGRenderer extends EventEmitter { return correctedMovement; } + // Toggles collapsed state of foreground_elem if applicable. + // Returns true if re-layout occured and re-draw is necessary. + public toggle_element_collapse(foreground_elem: any): boolean { + + const sdfg = (foreground_elem ? foreground_elem.sdfg : null); + let sdfg_elem = null; + if (foreground_elem instanceof State) { + sdfg_elem = foreground_elem.data.state; + } else if (foreground_elem instanceof ControlFlowRegion) { + sdfg_elem = foreground_elem.data.block; + } else if (foreground_elem instanceof SDFGNode) { + sdfg_elem = foreground_elem.data.node; + + // If a scope exit node, use entry instead + if (sdfg_elem.type.endsWith('Exit') && + foreground_elem.parent_id !== null) { + sdfg_elem = sdfg.nodes[foreground_elem.parent_id].nodes[ + sdfg_elem.scope_entry + ]; + } + } else { + sdfg_elem = null; + } + + // Toggle collapsed state + if (foreground_elem.COLLAPSIBLE) { + if ('is_collapsed' in sdfg_elem.attributes) { + sdfg_elem.attributes.is_collapsed = + !sdfg_elem.attributes.is_collapsed; + } else { + sdfg_elem.attributes['is_collapsed'] = true; + } + + this.emit('collapse_state_changed'); + + // Re-layout SDFG + this.add_loading_animation(); + setTimeout(() => { + this.relayout(); + }, 10); + + return true; + } + + return false; + } + // TODO(later): Improve event system using event types (instanceof) instead // of passing string eventtypes. /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @@ -2921,7 +2971,9 @@ export class SDFGRenderer extends EventEmitter { return false; } } else if (evtype === 'wheel') { - if (SDFVSettings.useVerticalScrollNavigation && !event.ctrlKey) { + if (SDFVSettings.useVerticalScrollNavigation && !event.ctrlKey + || !SDFVSettings.useVerticalScrollNavigation && event.ctrlKey + ) { // If vertical scroll navigation is turned on, use this to // move the viewport up and down. If the control key is held // down while scrolling, treat it as a typical zoom operation. @@ -3043,7 +3095,7 @@ export class SDFGRenderer extends EventEmitter { highlighting_changed = true; hover_changed = true; } - + // Highlight all edges of the memlet tree if (obj instanceof Edge && obj.parent_id !== null) { if (obj.hovered && hover_changed) { @@ -3100,8 +3152,46 @@ export class SDFGRenderer extends EventEmitter { } if (obj instanceof Connector) { - // Highlight all access nodes with the same name as - // the hovered connector in the nested sdfg. + + // Highlight the incoming/outgoing Edge + const parent_node = obj.linkedElem; + if (obj.hovered && (hover_changed || (!parent_node?.hovered))) { + const state = obj.linkedElem?.parentElem; + if (state && state instanceof State && state.data) { + const state_json = state.data.state; + const state_graph = state.data.graph; + state_json.edges.forEach((edge: JsonSDFGEdge, id: number) => { + if (edge.src_connector === obj.data.name || edge.dst_connector === obj.data.name) { + const gedge = state_graph.edge(edge.src, edge.dst, id.toString()) as Memlet; + if (gedge) { + gedge.highlighted = true; + } + } + }); + } + } + if (!obj.hovered && hover_changed) { + // Prevent de-highlighting of edge if parent is already hovered (to show all edges) + if (parent_node && !parent_node.hovered) { + const state = obj.linkedElem?.parentElem; + if (state && state instanceof State && state.data) { + const state_json = state.data.state; + const state_graph = state.data.graph; + state_json.edges.forEach((edge: JsonSDFGEdge, id: number) => { + if (edge.src_connector === obj.data.name || edge.dst_connector === obj.data.name) { + const gedge = state_graph.edge(edge.src, edge.dst, id.toString()) as Memlet; + if (gedge) { + gedge.highlighted = false; + } + } + }); + } + } + } + + + // Highlight all access nodes with the same name as the + // hovered connector in the nested sdfg if (obj.hovered && hover_changed) { const nGraph = obj.parentElem?.data.graph; if (nGraph) { @@ -3178,6 +3268,44 @@ export class SDFGRenderer extends EventEmitter { } } } + + // Make all edges of a node visible and remove the edge summary symbol + if (obj.hovered && hover_changed) { + // Setting these to false will cause the summary symbol + // not to be drawn in renderer_elements.ts + obj.summarize_in_edges = false; + obj.summarize_out_edges = false; + const state = obj.parentElem; + if (state && state instanceof State && state.data) { + const state_json = state.data.state; + const state_graph = state.data.graph; + state_json.edges.forEach((edge: JsonSDFGEdge, id: number) => { + if (edge.src === obj.id.toString() || edge.dst === obj.id.toString()) { + const gedge = state_graph.edge(edge.src, edge.dst, id.toString()) as Memlet; + if (gedge) { + gedge.highlighted = true; + } + } + }); + } + } + else if (!obj.hovered && hover_changed) { + obj.summarize_in_edges = true; + obj.summarize_out_edges = true; + const state = obj.parentElem; + if (state && state instanceof State && state.data) { + const state_json = state.data.state; + const state_graph = state.data.graph; + state_json.edges.forEach((edge: JsonSDFGEdge, id: number) => { + if (edge.src === obj.id.toString() || edge.dst === obj.id.toString()) { + const gedge = state_graph.edge(edge.src, edge.dst, id.toString()) as Memlet; + if (gedge) { + gedge.highlighted = false; + } + } + }); + } + } } ); @@ -3200,42 +3328,8 @@ export class SDFGRenderer extends EventEmitter { } if (evtype === 'dblclick') { - const sdfg = (foreground_elem ? foreground_elem.sdfg : null); - let sdfg_elem = null; - if (foreground_elem instanceof State) { - sdfg_elem = foreground_elem.data.state; - } else if (foreground_elem instanceof ControlFlowRegion) { - sdfg_elem = foreground_elem.data.block; - } else if (foreground_elem instanceof SDFGNode) { - sdfg_elem = foreground_elem.data.node; - - // If a scope exit node, use entry instead - if (sdfg_elem.type.endsWith('Exit') && - foreground_elem.parent_id !== null) { - sdfg_elem = sdfg.nodes[foreground_elem.parent_id].nodes[ - sdfg_elem.scope_entry - ]; - } - } else { - sdfg_elem = null; - } - - // Toggle collapsed state - if (foreground_elem.COLLAPSIBLE) { - if ('is_collapsed' in sdfg_elem.attributes) { - sdfg_elem.attributes.is_collapsed = - !sdfg_elem.attributes.is_collapsed; - } else { - sdfg_elem.attributes['is_collapsed'] = true; - } - - this.emit('collapse_state_changed'); - - // Re-layout SDFG - this.add_loading_animation(); - setTimeout(() => { - this.relayout(); - }, 10); + const relayout_happened = this.toggle_element_collapse(foreground_elem); + if (relayout_happened) { dirty = true; element_focus_changed = true; } @@ -3414,6 +3508,7 @@ export class SDFGRenderer extends EventEmitter { el.selected = true; }); + // Handle right-clicks if (evtype === 'contextmenu') { if (this.mouse_mode === 'move') { let elements_to_reset = [foreground_elem]; @@ -3509,6 +3604,17 @@ export class SDFGRenderer extends EventEmitter { if (this.panmode_btn?.onclick) this.panmode_btn?.onclick(event); } + else if (this.mouse_mode === 'pan') { + + // Shift + Rightclick to toggle expand/collapse + if (event.shiftKey) { + const relayout_happened = this.toggle_element_collapse(foreground_elem); + if (relayout_happened) { + dirty = true; + element_focus_changed = true; + } + } + } } const mouse_x = comp_x_func(event); @@ -4113,10 +4219,11 @@ function relayoutSDFGState( const g: DagreGraph = new dagre.graphlib.Graph({ multigraph: true }); // Set layout options and a simpler algorithm for large graphs. - const layoutOptions: any = { ranksep: 30 }; + const layoutOptions: any = { ranksep: SDFV.RANKSEP }; if (state.nodes.length >= 1000) layoutOptions.ranker = 'longest-path'; + layoutOptions.nodesep = SDFV.NODESEP; g.setGraph(layoutOptions); // Set an object for the graph label. @@ -4367,7 +4474,7 @@ function relayoutSDFGState( state.nodes.forEach((node: JsonSDFGNode, id: number) => { const gnode: any = g.node(id.toString()); if (!gnode || (omitAccessNodes && gnode instanceof AccessNode)) { - // Rgnore nodes that should not be drawn. + // Ignore nodes that should not be drawn. return; } const topleft = gnode.topleft(); @@ -4409,6 +4516,104 @@ function relayoutSDFGState( } }); + + // Re-order in_connectors for the edges to not intertwine + state.nodes.forEach((node: JsonSDFGNode, id: number) => { + const gnode: any = g.node(id.toString()); + if (!gnode || (omitAccessNodes && gnode instanceof AccessNode)) { + // Ignore nodes that should not be drawn. + return; + } + + // Summarize edges for NestedSDFGs and ScopeNodes + if (gnode instanceof NestedSDFG || gnode instanceof ScopeNode) { + const n_of_in_connectors = gnode.in_connectors.length; + const n_of_out_connectors = gnode.out_connectors.length; + + if (n_of_in_connectors > 10) { + gnode.summarize_in_edges = true; + gnode.in_summary_has_effect = true; + } + if (n_of_out_connectors > 10) { + gnode.summarize_out_edges = true; + gnode.out_summary_has_effect = true; + } + } + + const SPACING = SDFV.LINEHEIGHT; + const iConnLength = (SDFV.LINEHEIGHT + SPACING) * Object.keys( + node.attributes.layout.in_connectors + ).length - SPACING; + let iConnX = gnode.x - iConnLength / 2.0 + SDFV.LINEHEIGHT / 2.0; + + // Dictionary that saves the x coordinates of each connector's source node or source connector. + // This is later used to reorder the in_connectors based on the sources' x coordinates. + let sources_x_coordinates: { [key: string]: number } = {}; + + // For each in_connector, find the x coordinate of the source node connector + for (const c of gnode.in_connectors) { + state.edges.forEach((edge: JsonSDFGEdge, id: number) => { + if (edge.dst === gnode.id.toString() && edge.dst_connector === c.data.name) { + + // If in-edges are to be summarized, set Memlet.summarized + const gedge = g.edge(edge.src, edge.dst, id.toString()) as Memlet; + if (gedge && gnode.summarize_in_edges) { + gedge.summarized = true; + } + + const source_node: SDFGNode = g.node(edge.src); + if (source_node) { + + // If source node doesn't have out_connectors, take + // the source node's own x coordinate + if (source_node.out_connectors.length === 0) { + sources_x_coordinates[c.data.name] = source_node.x; + } + else { + // Find the corresponding out_connector and take its x coordinate + for (let i = 0; i < source_node.out_connectors.length; ++i) { + if (source_node.out_connectors[i].data.name === edge.src_connector) { + sources_x_coordinates[c.data.name] = source_node.out_connectors[i].x; + break; + } + } + } + } + } + }); + + } + + // Sort the dictionary by x coordinate values + let sources_x_coordinates_sorted = Object.entries(sources_x_coordinates); + sources_x_coordinates_sorted.sort((a, b) => a[1] - b[1]); + + // In the order of the sorted source x coordinates, set the x coordinates of the in_connectors + for (const element of sources_x_coordinates_sorted) { + for (const c of gnode.in_connectors) { + if (c.data.name === element[0]) { + c.x = iConnX; + iConnX += SDFV.LINEHEIGHT + SPACING; + continue; + } + } + } + + // For out_connectors set Memlet.summarized for all out-edges if needed + if (gnode.summarize_out_edges) { + for (const c of gnode.out_connectors) { + state.edges.forEach((edge: JsonSDFGEdge, id: number) => { + if (edge.src === gnode.id.toString() && edge.src_connector === c.data.name) { + const gedge = g.edge(edge.src, edge.dst, id.toString()) as Memlet; + if (gedge) { + gedge.summarized = true; + } + } + }); + } + } + }); + state.edges.forEach((edge: JsonSDFGEdge, id: number) => { const nedge = check_and_redirect_edge(edge, drawnNodes, state); if (!nedge) diff --git a/src/renderer/renderer_elements.ts b/src/renderer/renderer_elements.ts index 77d0615a..1784eb44 100644 --- a/src/renderer/renderer_elements.ts +++ b/src/renderer/renderer_elements.ts @@ -59,6 +59,19 @@ export class SDFGElement { public selected: boolean = false; public highlighted: boolean = false; public hovered: boolean = false; + + // Used to draw edge summary instead of all edges separately. + // Helps with rendering performance when too many edges would be drawn on the screen. + // These two fields get set in the layouter, depending on the number of in/out_connectors + // of a node. They also get toggled in the mousehandler when the hover status changes. + // Currently only used for NestedSDFGs and ScopeNodes. + public summarize_in_edges: boolean = false; + public summarize_out_edges: boolean = false; + // Used in draw_edge_summary to decide if edge summary is applicable. Set in the layouter + // only for NestedSDFGs and ScopeNodes. This prevents the summary to get toggled on + // by the mousehandler when it is not applicable. + public in_summary_has_effect: boolean = false; + public out_summary_has_effect: boolean = false; public x: number = 0; public y: number = 0; @@ -213,7 +226,133 @@ export class SDFGElement { ): string { return renderer.getCssProperty(propertyName); } + + public draw_edge_summary( + renderer: SDFGRenderer, ctx: CanvasRenderingContext2D + ): void { + + // Only draw if close enough + const canvas_manager = renderer.get_canvas_manager(); + const ppp = canvas_manager?.points_per_pixel(); + if (!(ctx as any).lod || (ppp && ppp < SDFV.EDGE_LOD)) { + const topleft = this.topleft(); + ctx.strokeStyle = this.strokeStyle(renderer); + ctx.fillStyle = ctx.strokeStyle; + + function draw_summary_symbol(ctx: CanvasRenderingContext2D, + min_connector_x: number, max_connector_x: number, + horizontal_line_level: number, draw_arrows_above_line: boolean + ): void { + + // Draw horizontal line (looks better without) + // ctx.beginPath(); + // ctx.moveTo(min_connector_x, horizontal_line_level); + // ctx.lineTo(max_connector_x, horizontal_line_level); + // ctx.closePath(); + // ctx.stroke(); + + // Draw left arrow + const middle_of_line = (min_connector_x + max_connector_x) / 2; + const left_arrow_x = middle_of_line - 10; + const righ_arrow_x = middle_of_line + 10; + let arrow_start_y = horizontal_line_level + 2; + let arrow_end_y = horizontal_line_level + 8; + if (draw_arrows_above_line) { + arrow_start_y = horizontal_line_level - 10; + arrow_end_y = horizontal_line_level - 4; + } + const dot_height = (arrow_start_y + arrow_end_y) / 2; + // Arrow line + ctx.beginPath(); + ctx.moveTo(left_arrow_x, arrow_start_y); + ctx.lineTo(left_arrow_x, arrow_end_y); + ctx.closePath(); + ctx.stroke(); + // Arrow head + ctx.beginPath(); + ctx.moveTo(left_arrow_x, arrow_end_y + 2); + ctx.lineTo(left_arrow_x - 2, arrow_end_y); + ctx.lineTo(left_arrow_x + 2, arrow_end_y); + ctx.lineTo(left_arrow_x, arrow_end_y + 2); + ctx.closePath(); + ctx.fill(); + + // 3 dots + ctx.beginPath(); + ctx.moveTo(middle_of_line - 5, dot_height) + ctx.lineTo(middle_of_line - 4, dot_height) + ctx.closePath(); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(middle_of_line - 0.5, dot_height) + ctx.lineTo(middle_of_line + 0.5, dot_height) + ctx.closePath(); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(middle_of_line + 4, dot_height) + ctx.lineTo(middle_of_line + 5, dot_height) + ctx.closePath(); + ctx.stroke(); + + // Draw right arrow + // Arrow line + ctx.beginPath(); + ctx.moveTo(righ_arrow_x, arrow_start_y); + ctx.lineTo(righ_arrow_x, arrow_end_y); + ctx.closePath(); + ctx.stroke(); + // Arrow head + ctx.beginPath(); + ctx.moveTo(righ_arrow_x, arrow_end_y + 2); + ctx.lineTo(righ_arrow_x - 2, arrow_end_y); + ctx.lineTo(righ_arrow_x + 2, arrow_end_y); + ctx.lineTo(righ_arrow_x, arrow_end_y + 2); + ctx.closePath(); + ctx.fill(); + } + + if (this.summarize_in_edges && this.in_summary_has_effect) { + // Find the left most and right most connector coordinates + if (this.in_connectors.length > 0) { + let min_connector_x = Number.MAX_SAFE_INTEGER; + let max_connector_x = Number.MIN_SAFE_INTEGER; + this.in_connectors.forEach((c: Connector) => { + if (c.x < min_connector_x) { + min_connector_x = c.x; + } + if (c.x > max_connector_x) { + max_connector_x = c.x; + } + }); + // Draw the summary symbol above the node + draw_summary_symbol(ctx, + min_connector_x, max_connector_x, + topleft.y - 8, true); + } + } + if (this.summarize_out_edges && this.out_summary_has_effect) { + // Find the left most and right most connector coordinates + if (this.out_connectors.length > 0) { + let min_connector_x = Number.MAX_SAFE_INTEGER; + let max_connector_x = Number.MIN_SAFE_INTEGER; + this.out_connectors.forEach((c: Connector) => { + if (c.x < min_connector_x) { + min_connector_x = c.x; + } + if (c.x > max_connector_x) { + max_connector_x = c.x; + } + }); + + // Draw the summary symbol below the node + draw_summary_symbol(ctx, + min_connector_x, max_connector_x, + topleft.y + this.height + 8, false); + } + } + } + } } // SDFG as an element (to support properties) @@ -1115,7 +1254,11 @@ export abstract class Edge extends SDFGElement { } -export class Memlet extends Edge { +export class Memlet extends Edge { + + // Currently used for Memlets to decide if they need to be drawn or not. + // Set in the layouter. + public summarized: boolean = false; public create_arrow_line(ctx: CanvasRenderingContext2D): void { // Draw memlet edges with quadratic curves through the arrow points. @@ -1385,7 +1528,7 @@ export class InterstateEdge extends Edge { const ppp = renderer.get_canvas_manager()?.points_per_pixel(); if (ppp === undefined) return; - if ((ctx as any).lod && ppp >= SDFV.SCOPE_LOD) + if ((ctx as any).lod && ppp > SDFV.SCOPE_LOD) return; const labelLines = []; @@ -1752,6 +1895,9 @@ export class ScopeNode extends SDFGNode { renderer: SDFGRenderer, ctx: CanvasRenderingContext2D, _mousepos?: Point2D ): void { + + this.draw_edge_summary(renderer, ctx); + let draw_shape; if (this.data.node.attributes.is_collapsed) { draw_shape = () => { @@ -2338,6 +2484,9 @@ export class NestedSDFG extends SDFGNode { renderer: SDFGRenderer, ctx: CanvasRenderingContext2D, mousepos?: Point2D ): void { + + this.draw_edge_summary(renderer, ctx); + if (this.data.node.attributes.is_collapsed) { const topleft = this.topleft(); drawOctagon(ctx, topleft, this.width, this.height); @@ -2604,6 +2753,10 @@ function batchedDrawEdges( deferredEdges.push(edge); return; } + // Dont draw if Memlet is summarized + else if (edge instanceof Memlet && edge.summarized) { + return; + } const lPoint = edge.points[edge.points.length - 1]; if (visible_rect && lPoint.x >= visible_rect.x && @@ -2669,16 +2822,17 @@ export function drawStateContents( )) continue; - if (node instanceof NestedSDFG) { - if (lod && ( - Math.sqrt(node.height * node.width) / ppp - ) < SDFV.STATE_LOD) { + // Simple draw for non-collapsed NestedSDFGs + if (node instanceof NestedSDFG && !node.data.node.attributes.is_collapsed) { + const nodeppp = Math.sqrt(node.width * node.height) / ppp; + if (lod && nodeppp < SDFV.STATE_LOD) { node.simple_draw(renderer, ctx, mousePos); node.debug_draw(renderer, ctx); // SDFGRenderer.rendered_elements_count++; continue; } } else { + // Simple draw node if (lod && ppp > SDFV.NODE_LOD) { node.simple_draw(renderer, ctx, mousePos); node.debug_draw(renderer, ctx); @@ -2919,7 +3073,7 @@ export function drawAdaptiveText( if (ppp === undefined) return; - const is_far: boolean = (ctx as any).lod && ppp >= ppp_thres; + const is_far: boolean = (ctx as any).lod && ppp > ppp_thres; const label = is_far ? far_text : close_text; let font_size = Math.min( diff --git a/src/sdfv.ts b/src/sdfv.ts index 89a0d4b2..7997fc85 100644 --- a/src/sdfv.ts +++ b/src/sdfv.ts @@ -60,13 +60,22 @@ export class SDFV { public static NODE_LOD: number = 5.0; // 5.0 // Points-per-pixel threshold for not drawing node text. public static TEXT_LOD: number = 1.5; // 1.5 - // Pixel threshold for not drawing state contents. + + // Pixel threshold for not drawing State and NestedSDFG contents. + // This threshold behaves differently than the ones above. The State's size is compared to this + // threshold and if the State is smaller its contents are not drawn in the renderer. public static STATE_LOD: number = 100; // 100 public static DEFAULT_CANVAS_FONTSIZE: number = 10; - public static DEFAULT_MAX_FONTSIZE: number = 20; // 50 + public static DEFAULT_MAX_FONTSIZE: number = 20; // 20 public static DEFAULT_FAR_FONT_MULTIPLIER: number = 16; // 16 + // Dagre layout options. + // Separation between ranks (vertically) in pixels. + public static RANKSEP: number = 70; // Dagre default: 50 + // Separation between nodes (horizontally) in pixels. + public static NODESEP: number = 20; // Dagre default: 50 + protected renderer: SDFGRenderer | null = null; protected localViewRenderer: LViewRenderer | null = null; @@ -253,11 +262,10 @@ export class SDFV { const contents = $(contentsRaw); contents.html(''); - if (elem instanceof Memlet && elem.parent_id && elem.id) { - const sdfg_edge = elem.parentElem?.data.state.edges[elem.id]; - contents.append($('

    ', { - html: 'Connectors: ' + sdfg_edge.src_connector + ' → ' + - sdfg_edge.dst_connector, + if (elem instanceof Memlet) { + contents.append($('

    ', { + html: 'Connectors: ' + elem.src_connector + ' → ' + + elem.dst_connector, })); } contents.append($('


    ')); @@ -545,7 +553,7 @@ function file_read_complete(sdfv: SDFV): void { new SDFGRenderer(sdfv, sdfg, container, mouse_event) ); sdfv.close_menu(); - }, 20); + }, 10); } }