From 9c7dea8e2f7673951c4d52eaff18fd4fbb4f9f8c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sat, 22 Aug 2015 17:14:32 +0200 Subject: [PATCH] cache render engines for consistent renderings * one render engine per topology * improves switching back and forth between topos * might increase memory use * roughly fixes #160 --- client/app/scripts/charts/nodes-chart.js | 15 ++++-- client/app/scripts/charts/nodes-layout.js | 57 ++++++++++++++--------- client/app/scripts/components/app.js | 4 +- client/app/scripts/components/nodes.js | 1 + client/app/scripts/stores/app-store.js | 4 ++ 5 files changed, 52 insertions(+), 29 deletions(-) diff --git a/client/app/scripts/charts/nodes-chart.js b/client/app/scripts/charts/nodes-chart.js index 952eff3e51..2fe09476c2 100644 --- a/client/app/scripts/charts/nodes-chart.js +++ b/client/app/scripts/charts/nodes-chart.js @@ -133,11 +133,10 @@ const NodesChart = React.createClass({ _.each(topology, function(node, id) { nodes[id] = prevNodes[id] || {}; - // initialize position for new nodes + // use cached positions if available _.defaults(nodes[id], { x: centerX, - y: centerY, - textAnchor: 'start' + y: centerY }); // copy relevant fields to state nodes @@ -184,12 +183,17 @@ const NodesChart = React.createClass({ }, updateGraphState: function(props) { + const n = _.size(props.nodes); + + if (n === 0) { + return; + } + const nodes = this.initNodes(props.nodes, this.state.nodes); const edges = this.initEdges(props.nodes, nodes); const expanse = Math.min(props.height, props.width); const nodeSize = expanse / 2; - const n = _.size(props.nodes); const nodeScale = d3.scale.linear().range([0, nodeSize / Math.pow(n, 0.7)]); const timedLayouter = timely(NodesLayout.doLayout); @@ -199,7 +203,8 @@ const NodesChart = React.createClass({ props.width, props.height, nodeScale, - MARGINS + MARGINS, + this.props.topologyId ); debug('graph layout took ' + timedLayouter.time + 'ms'); diff --git a/client/app/scripts/charts/nodes-layout.js b/client/app/scripts/charts/nodes-layout.js index 10ce9bfe8a..456b87dbfc 100644 --- a/client/app/scripts/charts/nodes-layout.js +++ b/client/app/scripts/charts/nodes-layout.js @@ -4,78 +4,89 @@ const Naming = require('../constants/naming'); const _ = require('lodash'); const MAX_NODES = 100; -const g = new dagre.graphlib.Graph({}); +const topologyGraphs = {}; -const doLayout = function(nodes, edges, width, height, scale, margins) { +const doLayout = function(nodes, edges, width, height, scale, margins, topologyId) { let offsetX = 0 + margins.left; let offsetY = 0 + margins.top; + let graph; if (_.size(nodes) > MAX_NODES) { debug('Too many nodes for graph layout engine. Limit: ' + MAX_NODES); return null; } + // one engine per topology, to keep renderings similar + if (!topologyGraphs[topologyId]) { + topologyGraphs[topologyId] = new dagre.graphlib.Graph({}); + } + graph = topologyGraphs[topologyId]; + // configure node margins - g.setGraph({ + graph.setGraph({ nodesep: scale(2.5), ranksep: scale(2.5) }); // add nodes to the graph if not already there _.each(nodes, function(node) { - if (!g.hasNode(node.id)) { - g.setNode(node.id, {id: node.id, width: scale(0.75), height: scale(0.75)}); + if (!graph.hasNode(node.id)) { + graph.setNode(node.id, { + id: node.id, + width: scale(0.75), + height: scale(0.75) + }); } }); // remove nodes that are no longer there - _.each(g.nodes(), function(nodeid) { + _.each(graph.nodes(), function(nodeid) { if (!_.has(nodes, nodeid)) { - g.removeNode(nodeid); + graph.removeNode(nodeid); } }); // add edges to the graph if not already there _.each(edges, function(edge) { - if (!g.hasEdge(edge.source.id, edge.target.id)) { + if (!graph.hasEdge(edge.source.id, edge.target.id)) { const virtualNodes = edge.source.id === edge.target.id ? 1 : 0; - g.setEdge(edge.source.id, edge.target.id, {id: edge.id, minlen: virtualNodes}); + graph.setEdge(edge.source.id, edge.target.id, {id: edge.id, minlen: virtualNodes}); } }); // remoed egdes that are no longer there - _.each(g.edges(), function(edgeObj) { + _.each(graph.edges(), function(edgeObj) { const edge = [edgeObj.v, edgeObj.w]; const edgeId = edge.join(Naming.EDGE_ID_SEPARATOR); if (!_.has(edges, edgeId)) { - g.removeEdge(edgeObj.v, edgeObj.w); + graph.removeEdge(edgeObj.v, edgeObj.w); } }); - dagre.layout(g); + dagre.layout(graph); - const graph = g.graph(); + const layout = graph.graph(); // shifting graph coordinates to center - if (graph.width < width) { - offsetX = (width - graph.width) / 2 + margins.left; + if (layout.width < width) { + offsetX = (width - layout.width) / 2 + margins.left; } - if (graph.height < height) { - offsetY = (height - graph.height) / 2 + margins.top; + if (layout.height < height) { + offsetY = (height - layout.height) / 2 + margins.top; } // apply coordinates to nodes and edges - g.nodes().forEach(function(id) { + graph.nodes().forEach(function(id) { const node = nodes[id]; - const graphNode = g.node(id); + const graphNode = graph.node(id); node.x = graphNode.x + offsetX; node.y = graphNode.y + offsetY; }); - g.edges().forEach(function(id) { - const graphEdge = g.edge(id); + graph.edges().forEach(function(id) { + const graphEdge = graph.edge(id); const edge = edges[graphEdge.id]; _.each(graphEdge.points, function(point) { point.x += offsetX; @@ -84,9 +95,9 @@ const doLayout = function(nodes, edges, width, height, scale, margins) { edge.points = graphEdge.points; }); - // return object with width and height of layout + // return object with the width and height of layout - return graph; + return layout; }; module.exports = { diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index a9b0899d2c..2902e8a421 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -15,6 +15,7 @@ const ESC_KEY_CODE = 27; function getStateFromStores() { return { currentTopology: AppStore.getCurrentTopology(), + currentTopologyId: AppStore.getCurrentTopologyId(), errorUrl: AppStore.getErrorUrl(), highlightedEdgeIds: AppStore.getHighlightedEdgeIds(), highlightedNodeIds: AppStore.getHighlightedNodeIds(), @@ -70,7 +71,8 @@ const App = React.createClass({ + highlightedEdgeIds={this.state.highlightedEdgeIds} + topologyId={this.state.currentTopologyId} />
{versionString}   diff --git a/client/app/scripts/components/nodes.js b/client/app/scripts/components/nodes.js index 4d1896d867..9550f55cdb 100644 --- a/client/app/scripts/components/nodes.js +++ b/client/app/scripts/components/nodes.js @@ -37,6 +37,7 @@ const Nodes = React.createClass({ onNodeClick={this.onNodeClick} width={this.state.width} height={this.state.height} + topologyId={this.props.topologyId} context="view" />
diff --git a/client/app/scripts/stores/app-store.js b/client/app/scripts/stores/app-store.js index 3f5c002533..1f52041663 100644 --- a/client/app/scripts/stores/app-store.js +++ b/client/app/scripts/stores/app-store.js @@ -57,6 +57,10 @@ const AppStore = assign({}, EventEmitter.prototype, { return findCurrentTopology(topologies, currentTopologyId); }, + getCurrentTopologyId: function() { + return currentTopologyId; + }, + getCurrentTopologyUrl: function() { const topology = this.getCurrentTopology();