summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/resources/discards
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/browser/resources/discards')
-rw-r--r--chromium/chrome/browser/resources/discards/OWNERS2
-rw-r--r--chromium/chrome/browser/resources/discards/discards_tab.html20
-rw-r--r--chromium/chrome/browser/resources/discards/discards_tab.js48
-rw-r--r--chromium/chrome/browser/resources/discards/graph_doc.js265
-rw-r--r--chromium/chrome/browser/resources/discards/graph_doc_template.html27
5 files changed, 244 insertions, 118 deletions
diff --git a/chromium/chrome/browser/resources/discards/OWNERS b/chromium/chrome/browser/resources/discards/OWNERS
index dcd54320481..6ef4e6df1e5 100644
--- a/chromium/chrome/browser/resources/discards/OWNERS
+++ b/chromium/chrome/browser/resources/discards/OWNERS
@@ -1 +1 @@
-file://services/resource_coordinator/OWNERS
+file://components/performance_manager/OWNERS
diff --git a/chromium/chrome/browser/resources/discards/discards_tab.html b/chromium/chrome/browser/resources/discards/discards_tab.html
index b91684d1b52..c4d1e25566a 100644
--- a/chromium/chrome/browser/resources/discards/discards_tab.html
+++ b/chromium/chrome/browser/resources/discards/discards_tab.html
@@ -206,11 +206,6 @@
</div>
</div>
</th>
- <th data-sort-key="canFreeze" on-click="onSortClick">
- <div class="header-cell-container">
- Can freeze?
- </div>
- </th>
<th data-sort-key="discardCount" on-click="onSortClick">
<div class="header-cell-container">
<div>
@@ -258,19 +253,6 @@
<td>[[visibilityToString_(item.visibility)]]</td>
<td>[[loadingStateToString_(item.loadingState)]]</td>
<td>[[getLifeCycleState_(item)]]</td>
- <td class="boolean-cell">
- <div>[[boolToString_(item.canFreeze)]]</div>
- <div is="action-link" class="tooltip-container"
- disabled$="[[!hasCannotFreezeReasons_(item)]]">
- [View Reason]
- <div class="tooltip">
- <template is="dom-repeat"
- items="[[item.cannotFreezeReasons]]">
- <div>[[item]]</div>
- </template>
- </div>
- </div>
- </td>
<td>[[item.discardCount]]</td>
<td class="boolean-cell">
<div>[[boolToString_(item.isAutoDiscardable)]]</div>
@@ -286,8 +268,6 @@
<div is="action-link" on-click="loadTab_"
disabled$="[[!canLoad_(item)]]">
[Load]</div>
- <div is="action-link" on-click="freezeTab_"
- disabled$="[[!canFreeze_(item)]]">[Freeze]</div>
<div is="action-link" on-click="urgentDiscardTab_"
disabled$="[[!canDiscard_(item)]]">
[Urgent Discard]
diff --git a/chromium/chrome/browser/resources/discards/discards_tab.js b/chromium/chrome/browser/resources/discards/discards_tab.js
index 2fef49056b5..963023218e5 100644
--- a/chromium/chrome/browser/resources/discards/discards_tab.js
+++ b/chromium/chrome/browser/resources/discards/discards_tab.js
@@ -37,7 +37,7 @@ export function compareTabDiscardsInfos(sortKey, a, b) {
}
// Compares boolean fields.
- if (['canFreeze', 'isAutoDiscardable'].includes(sortKey)) {
+ if (['isAutoDiscardable'].includes(sortKey)) {
if (val1 === val2) {
return 0;
}
@@ -219,10 +219,6 @@ Polymer({
return pageLifecycleStateFromVisibilityAndFocus();
case mojom.LifecycleUnitState.THROTTLED:
return pageLifecycleStateFromVisibilityAndFocus() + ' (throttled)';
- case mojom.LifecycleUnitState.PENDING_FREEZE:
- return pageLifecycleStateFromVisibilityAndFocus() + ' (pending frozen)';
- case mojom.LifecycleUnitState.FROZEN:
- return 'frozen';
case mojom.LifecycleUnitState.DISCARDED:
return 'discarded (' + this.discardReasonToString_(reason) + ')' +
((reason === mojom.LifecycleUnitDiscardReason.URGENT) ? ' at ' +
@@ -231,8 +227,6 @@ Polymer({
(new Date(stateChangeTime.microseconds / 1000))
.toLocaleString() :
'');
- case mojom.LifecycleUnitState.PENDING_UNFREEZE:
- return 'frozen (pending unfreeze)';
}
assertNotReached('Unknown lifecycle state: ' + state);
},
@@ -331,16 +325,6 @@ Polymer({
},
/**
- * Tests whether an item has reasons why it cannot be frozen.
- * @param {discards.mojom.TabDiscardsInfo} item The item in question.
- * @return {boolean} true iff there are reasons why the item cannot be
- * frozen.
- * @private
- */
- hasCannotFreezeReasons_(item) {
- return item.cannotFreezeReasons.length !== 0;
- },
- /**
* Tests whether an item has reasons why it cannot be discarded.
* @param {discards.mojom.TabDiscardsInfo} item The item in question.
* @return {boolean} true iff there are reasons why the item cannot be
@@ -362,27 +346,6 @@ Polymer({
},
/**
- * Tests whether an item can be frozen.
- * @param {discards.mojom.TabDiscardsInfo} item The item in question.
- * @return {boolean} true iff the item can be frozen.
- * @private
- */
- canFreeze_(item) {
- if (item.visibility === discards.mojom.LifecycleUnitVisibility.HIDDEN ||
- item.visibility === discards.mojom.LifecycleUnitVisibility.OCCLUDED) {
- // Only tabs that aren't visible can be frozen for now.
- switch (item.state) {
- case mojom.LifecycleUnitState.DISCARDED:
- case mojom.LifecycleUnitState.FROZEN:
- case mojom.LifecycleUnitState.PENDING_FREEZE:
- return false;
- }
- return true;
- }
- return false;
- },
-
- /**
* Tests whether an item can be discarded.
* @param {discards.mojom.TabDiscardsInfo} item The item in question.
* @return {boolean} true iff the item can be discarded.
@@ -423,15 +386,6 @@ Polymer({
},
/**
- * Event handler that freezes a tab.
- * @param {Event} e The event.
- * @private
- */
- freezeTab_(e) {
- this.discardsDetailsProvider_.freezeById(e.model.item.id);
- },
-
- /**
* Event handler that discards a given tab urgently.
* @param {Event} e The event.
* @private
diff --git a/chromium/chrome/browser/resources/discards/graph_doc.js b/chromium/chrome/browser/resources/discards/graph_doc.js
index 37b9bc7d46d..e3ce7e1d7f8 100644
--- a/chromium/chrome/browser/resources/discards/graph_doc.js
+++ b/chromium/chrome/browser/resources/discards/graph_doc.js
@@ -2,25 +2,42 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+// Radius of a node circle.
+const /** number */ kNodeRadius = 6;
+
// Target y position for page nodes.
-const kPageNodesTargetY = 20;
+const /** number */ kPageNodesTargetY = 20;
// Range occupied by page nodes at the top of the graph view.
-const kPageNodesYRange = 100;
+const /** number */ kPageNodesYRange = 100;
// Range occupied by process nodes at the bottom of the graph view.
-const kProcessNodesYRange = 100;
+const /** number */ kProcessNodesYRange = 100;
// Range occupied by worker nodes at the bottom of the graph view, above
// process nodes.
-const kWorkerNodesYRange = 200;
+const /** number */ kWorkerNodesYRange = 200;
// Target y position for frame nodes.
-const kFrameNodesTargetY = kPageNodesYRange + 50;
+const /** number */ kFrameNodesTargetY = kPageNodesYRange + 50;
// Range that frame nodes cannot enter at the top/bottom of the graph view.
-const kFrameNodesTopMargin = kPageNodesYRange;
-const kFrameNodesBottomMargin = kWorkerNodesYRange + 50;
+const /** number */ kFrameNodesTopMargin = kPageNodesYRange;
+const /** number */ kFrameNodesBottomMargin = kWorkerNodesYRange + 50;
+
+// The maximum strength of a boundary force.
+// According to https://github.com/d3/d3-force#positioning, strength values
+// outside the range [0,1] are "not recommended".
+const /** number */ kMaxBoundaryStrength = 1;
+
+// The strength of a high Y-force. This is appropriate for forces that
+// strongly pull towards an attractor, but can still be overridden by the
+// strongest force.
+const /** number */ kHighYStrength = 0.9;
+
+// The strength of a weak Y-force. This is appropriate for forces that exert
+// some influence but can be easily overridden.
+const /** number */ kWeakYStrength = 0.1;
class ToolTip {
/**
@@ -271,10 +288,18 @@ class GraphNode {
/**
* @return {number} The strength of the force that pulls the node towards
- * its target y position.
+ * its target y position.
+ */
+ get targetYPositionStrength() {
+ return kWeakYStrength;
+ }
+
+ /**
+ * @return {number} A scaling factor applied to the strength of links to this
+ * node.
*/
- targetYPositionStrength() {
- return 0.1;
+ get linkStrengthScalingFactor() {
+ return 1;
}
/**
@@ -287,12 +312,23 @@ class GraphNode {
}
/** @return {number} The strength of the repulsion force with other nodes. */
- manyBodyStrength() {
+ get manyBodyStrength() {
return -200;
}
- /** @return {!Array<number>} */
- linkTargets() {
+ /** @return {!Array<number>} an array of node ids. */
+ get linkTargets() {
+ return [];
+ }
+
+ /**
+ * Dashed links express ownership relationships. An object can own multiple
+ * things, but be owned by exactly one (per relationship type). As such, the
+ * relationship is expressed on the *owned* object. These links are drawn with
+ * an arrow at the beginning of the link, pointing to the owned object.
+ * @return {!Array<number>} an array of node ids.
+ */
+ get dashedLinkTargets() {
return [];
}
@@ -322,8 +358,18 @@ class PageNode extends GraphNode {
}
/** @override */
- targetYPositionStrength() {
- return 10;
+ get targetYPositionStrength() {
+ // Gravitate strongly towards the top of the graph. Can be overridden by
+ // the bounding force which uses kMaxBoundaryStrength.
+ return kHighYStrength;
+ }
+
+ /** @override */
+ get linkStrengthScalingFactor() {
+ // Give links from frame nodes to page nodes less weight than links between
+ // frame nodes, so the that Y forces pulling page nodes into their area can
+ // dominate over link forces pulling them towards frame nodes.
+ return 0.5;
}
/** override */
@@ -332,9 +378,17 @@ class PageNode extends GraphNode {
}
/** override */
- manyBodyStrength() {
+ get manyBodyStrength() {
return -600;
}
+
+ /** override */
+ get dashedLinkTargets() {
+ if (this.page.openerFrameId) {
+ return [this.page.openerFrameId];
+ }
+ return [];
+ }
}
class FrameNode extends GraphNode {
@@ -362,7 +416,7 @@ class FrameNode extends GraphNode {
}
/** override */
- linkTargets() {
+ get linkTargets() {
// Only link to the page if there isn't a parent frame.
return [
this.frame.parentFrameId || this.frame.pageId, this.frame.processId
@@ -386,8 +440,18 @@ class ProcessNode extends GraphNode {
}
/** @return {number} */
- targetYPositionStrength() {
- return 10;
+ get targetYPositionStrength() {
+ // Gravitate strongly towards the bottom of the graph. Can be overridden by
+ // the bounding force which uses kMaxBoundaryStrength.
+ return kHighYStrength;
+ }
+
+ /** @override */
+ get linkStrengthScalingFactor() {
+ // Give links to process nodes less weight than links between frame nodes,
+ // so the that Y forces pulling process nodes into their area can dominate
+ // over link forces pulling them towards frame nodes.
+ return 0.5;
}
/** override */
@@ -396,7 +460,7 @@ class ProcessNode extends GraphNode {
}
/** override */
- manyBodyStrength() {
+ get manyBodyStrength() {
return -600;
}
}
@@ -417,8 +481,10 @@ class WorkerNode extends GraphNode {
}
/** @return {number} */
- targetYPositionStrength() {
- return 10;
+ get targetYPositionStrength() {
+ // Gravitate strongly towards the worker area of the graph. Can be
+ // overridden by the bounding force which uses kMaxBoundaryStrength.
+ return kHighYStrength;
}
/** override */
@@ -429,12 +495,12 @@ class WorkerNode extends GraphNode {
}
/** override */
- manyBodyStrength() {
+ get manyBodyStrength() {
return -600;
}
/** override */
- linkTargets() {
+ get linkTargets() {
// Link the process, in addition to all the client and child workers.
return [
this.worker.processId, ...this.worker.clientFrameIds,
@@ -444,14 +510,19 @@ class WorkerNode extends GraphNode {
}
/**
- * A force that bounds GraphNodes |allowedYRange| in Y.
+ * A force that bounds GraphNodes |allowedYRange| in Y,
+ * as well as bounding them to stay in page bounds in X.
* @param {number} graphHeight
+ * @param {number} graphWidth
*/
-function boundingForce(graphHeight) {
+function boundingForce(graphHeight, graphWidth) {
/** @type {!Array<!GraphNode>} */
let nodes = [];
/** @type {!Array<!Array>} */
let bounds = [];
+ const xBounds = [2 * kNodeRadius, graphWidth - 2 * kNodeRadius];
+ const boundPosition = (pos, bound) =>
+ Math.max(bound[0], Math.min(pos, bound[1]));
/** @param {number} alpha */
function force(alpha) {
@@ -459,12 +530,23 @@ function boundingForce(graphHeight) {
for (let i = 0; i < n; ++i) {
const bound = bounds[i];
const node = nodes[i];
- const yOld = node.y;
- const yNew = Math.max(bound[0], Math.min(yOld, bound[1]));
- if (yOld !== yNew) {
- node.y = yNew;
- // Zero the velocity of clamped nodes.
- node.vy = 0;
+
+ // Calculate where the node will end up after movement. If it will be out
+ // of bounds apply a counter-force to bring it back in.
+ const yNextPosition = node.y + node.vy;
+ const yBoundedPosition = boundPosition(yNextPosition, bound);
+ if (yNextPosition !== yBoundedPosition) {
+ // Do not include alpha because we want to be strongly repelled from
+ // the boundary even if alpha has decayed.
+ node.vy += (yBoundedPosition - yNextPosition) * kMaxBoundaryStrength;
+ }
+
+ const xNextPosition = node.x + node.vx;
+ const xBoundedPosition = boundPosition(xNextPosition, xBounds);
+ if (xNextPosition !== xBoundedPosition) {
+ // Do not include alpha because we want to be strongly repelled from
+ // the boundary even if alpha has decayed.
+ node.vx += (xBoundedPosition - xNextPosition) * kMaxBoundaryStrength;
}
}
}
@@ -472,7 +554,13 @@ function boundingForce(graphHeight) {
/** @param {!Array<!GraphNode>} n */
force.initialize = function(n) {
nodes = n;
- bounds = nodes.map(node => node.allowedYRange(graphHeight));
+ bounds = nodes.map(node => {
+ const nodeBounds = node.allowedYRange(graphHeight);
+ // Leave space for the node circle plus a small border.
+ nodeBounds[0] += kNodeRadius * 2;
+ nodeBounds[1] -= kNodeRadius * 2;
+ return nodeBounds;
+ });
};
return force;
@@ -533,6 +621,12 @@ class Graph {
*/
this.linkGroup_ = null;
+ /**
+ * A selection for the top-level <g> node that contains all dashed edges.
+ * @private {d3.selection}
+ */
+ this.dashedLinkGroup_ = null;
+
/** @private {!Map<number, !GraphNode>} */
this.nodes_ = new Map();
@@ -543,6 +637,12 @@ class Graph {
this.links_ = [];
/**
+ * The dashed links.
+ * @private {!Array<!d3.ForceLink>}
+ */
+ this.dashedLinks_ = [];
+
+ /**
* The host window.
* @private {Window}
*/
@@ -575,7 +675,17 @@ class Graph {
simulation.on('tick', this.onTick_.bind(this));
const linkForce = d3.forceLink().id(d => d.id);
- simulation.force('link', linkForce);
+ const defaultStrength = linkForce.strength();
+
+ // Override the default link strength function to apply scaling factors
+ // from the source and target nodes to the link strength. This lets
+ // different node types balance link forces with other forces that act on
+ // them.
+ simulation.force(
+ 'link',
+ linkForce.strength(
+ l => defaultStrength(l) * l.source.linkStrengthScalingFactor *
+ l.target.linkStrengthScalingFactor));
// Sets the repulsion force between nodes (positive number is attraction,
// negative number is repulsion).
@@ -588,8 +698,9 @@ class Graph {
// Create the <g> elements that host nodes and links.
// The link groups are created first so that all links end up behind nodes.
const svg = d3.select(this.svg_);
- this.toolTipLinkGroup_ = svg.append('g').attr('class', 'toolTipLinks');
+ this.toolTipLinkGroup_ = svg.append('g').attr('class', 'tool-tip-links');
this.linkGroup_ = svg.append('g').attr('class', 'links');
+ this.dashedLinkGroup_ = svg.append('g').attr('class', 'dashed-links');
this.nodeGroup_ = svg.append('g').attr('class', 'nodes');
this.separatorGroup_ = svg.append('g').attr('class', 'separators');
@@ -630,7 +741,11 @@ class Graph {
/** @override */
pageChanged(page) {
const pageNode = /** @type {!PageNode} */ (this.nodes_.get(page.id));
+
+ // Page node dashed links may change dynamically, so account for that here.
+ this.removeDashedNodeLinks_(pageNode);
pageNode.page = page;
+ this.addDashedNodeLinks_(pageNode);
}
/** @override */
@@ -665,6 +780,7 @@ class Graph {
// Remove any links, and then the node itself.
this.removeNodeLinks_(node);
+ this.removeDashedNodeLinks_(node);
this.nodes_.delete(nodeId);
}
@@ -709,12 +825,22 @@ class Graph {
* @private
*/
removeNodeLinks_(node) {
- // Filter away any links to or from the deleted node.
+ // Filter away any links to or from the provided node.
this.links_ = this.links_.filter(
link => link.source !== node && link.target !== node);
}
/**
+ * @param {!GraphNode} node
+ * @private
+ */
+ removeDashedNodeLinks_(node) {
+ // Filter away any dashed links to or from the provided node.
+ this.dashedLinks_ = this.dashedLinks_.filter(
+ link => link.source !== node && link.target !== node);
+ }
+
+ /**
* @param {!Object<string>} nodeDescriptions
* @private
*/
@@ -848,10 +974,18 @@ class Graph {
// Select the links.
const link = this.linkGroup_.selectAll('line').data(this.links_);
// Add new links.
- link.enter().append('line').attr('stroke-width', 1);
+ link.enter().append('line');
// Remove dead links.
link.exit().remove();
+ // Select the dashed links.
+ const dashedLink =
+ this.dashedLinkGroup_.selectAll('line').data(this.dashedLinks_);
+ // Add new dashed links.
+ dashedLink.enter().append('line');
+ // Remove dead dashed links.
+ dashedLink.exit().remove();
+
// Select the nodes, except for any dead ones that are still transitioning.
const nodes = Array.from(this.nodes_.values());
const node =
@@ -863,8 +997,10 @@ class Graph {
.append('g')
.call(this.drag_)
.on('click', this.onGraphNodeClick_.bind(this));
- const circles = newNodes.append('circle').attr('r', 9).attr(
- 'fill', 'green'); // New nodes appear green.
+ const circles = newNodes.append('circle')
+ .attr('id', d => `circle-${d.id}`)
+ .attr('r', kNodeRadius * 1.5)
+ .attr('fill', 'green'); // New nodes appear green.
newNodes.append('image')
.attr('x', -8)
@@ -877,7 +1013,7 @@ class Graph {
circles.transition()
.duration(2000)
.attr('fill', d => d.color)
- .attr('r', 6);
+ .attr('r', kNodeRadius);
}
if (!node.exit().empty()) {
@@ -913,9 +1049,11 @@ class Graph {
// Update and restart the simulation if the graph changed.
if (!node.enter().empty() || !node.exit().empty() ||
- !link.enter().empty() || !link.exit().empty()) {
+ !link.enter().empty() || !link.exit().empty() ||
+ !dashedLink.enter().empty() || !dashedLink.exit().empty()) {
this.simulation_.nodes(nodes);
- this.simulation_.force('link').links(this.links_);
+ const links = this.links_.concat(this.dashedLinks_);
+ this.simulation_.force('link').links(links);
this.restartSimulation_();
}
@@ -932,6 +1070,12 @@ class Graph {
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
+ const dashedLines = this.dashedLinkGroup_.selectAll('line');
+ dashedLines.attr('x1', d => d.source.x)
+ .attr('y1', d => d.source.y)
+ .attr('x2', d => d.target.x)
+ .attr('y2', d => d.target.y);
+
this.updateToolTipLinks();
}
@@ -945,6 +1089,7 @@ class Graph {
addNode_(node) {
this.nodes_.set(node.id, node);
this.addNodeLinks_(node);
+ this.addDashedNodeLinks_(node);
node.setInitialPosition(this.width_, this.height_);
}
@@ -955,8 +1100,7 @@ class Graph {
* @private
*/
addNodeLinks_(node) {
- const linkTargets = node.linkTargets();
- for (const linkTarget of linkTargets) {
+ for (const linkTarget of node.linkTargets) {
const target = this.nodes_.get(linkTarget);
if (target) {
this.links_.push({source: node, target: target});
@@ -965,6 +1109,21 @@ class Graph {
}
/**
+ * Adds all the dashed links for a node to the graph.
+ *
+ * @param {!GraphNode} node
+ * @private
+ */
+ addDashedNodeLinks_(node) {
+ for (const dashedLinkTarget of node.dashedLinkTargets) {
+ const target = this.nodes_.get(dashedLinkTarget);
+ if (target) {
+ this.dashedLinks_.push({source: node, target: target});
+ }
+ }
+ }
+
+ /**
* @param {!GraphNode} d The dragged node.
* @private
*/
@@ -993,8 +1152,16 @@ class Graph {
if (!d3.event.active) {
this.simulation_.alphaTarget(0);
}
- d.fx = null;
- d.fy = null;
+ // Leave the node pinned where it was dropped. Return it to free
+ // positioning if it's dropped outside its designated area.
+ const bounds = d.allowedYRange(this.height_);
+ if (d3.event.y < bounds[0] || d3.event.y > bounds[1]) {
+ d.fx = null;
+ d.fy = null;
+ }
+
+ // Toggle the pinned class as appropriate for the circle backing this node.
+ d3.select(`#circle-${d.id}`).classed('pinned', d.fx != null);
}
/**
@@ -1010,7 +1177,7 @@ class Graph {
* @private
*/
getTargetYPositionStrength_(d) {
- return d.targetYPositionStrength();
+ return d.targetYPositionStrength;
}
/**
@@ -1018,7 +1185,7 @@ class Graph {
* @private
*/
getManyBodyStrength_(d) {
- return d.manyBodyStrength();
+ return d.manyBodyStrength;
}
/**
@@ -1093,7 +1260,7 @@ class Graph {
.strength(this.getTargetYPositionStrength_.bind(this));
this.simulation_.force('x_pos', xForce);
this.simulation_.force('y_pos', yForce);
- this.simulation_.force('y_bound', boundingForce(this.height_));
+ this.simulation_.force('y_bound', boundingForce(this.height_, this.width_));
if (!this.wasResized_) {
this.wasResized_ = true;
diff --git a/chromium/chrome/browser/resources/discards/graph_doc_template.html b/chromium/chrome/browser/resources/discards/graph_doc_template.html
index b9fdfb27c8d..bcdb842d0c0 100644
--- a/chromium/chrome/browser/resources/discards/graph_doc_template.html
+++ b/chromium/chrome/browser/resources/discards/graph_doc_template.html
@@ -21,6 +21,20 @@ URL. As result, this document needs to be self-contained, hence inline scripts.
.links line {
stroke: #999;
stroke-opacity: 0.6;
+ stroke-width: 1;
+ }
+
+ .dashed-links line {
+ marker-start: url(#arrowToSource);
+ stroke: #999;
+ stroke-dasharray: 3;
+ stroke-opacity: 0.6;
+ stroke-width: 1;
+ }
+
+ #arrowToSource {
+ fill: #999;
+ stroke: #999;
}
.nodes circle {
@@ -28,6 +42,10 @@ URL. As result, this document needs to be self-contained, hence inline scripts.
stroke-width: 1.5px;
}
+ .nodes circle.pinned {
+ stroke: red;
+ }
+
.dead image {
display: none;
}
@@ -70,6 +88,13 @@ ${javascript_file}
</head>
<body>
<div id="toolTips" width="100%" height="100%"></div>
- <svg id="graphBody" width="100%" height="100%"></svg>
+ <svg id="graphBody" width="100%" height="100%">
+ <defs>
+ <marker id="arrowToSource" viewBox="0 -5 10 10" refX="-12" refY="0"
+ markerWidth="9" markerHeight="6" orient="auto">
+ <path d="M15,-7 L0,0 L15,7" />
+ </marker>
+ </defs>
+ </svg>
</body>
</html>