Skip to content

Commit b52d37a

Browse files
authored
Merge pull request #161 from magjac/add-support-for-shared-worker-option-and-destroy-method
Add support for shared worker option and destroy method
2 parents afe2d4d + a2ba01c commit b52d37a

15 files changed

Lines changed: 470 additions & 61 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
* New option *useSharedWorker* that enables the use of a [shared web worker](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker). The default is still to use a [dedicated web worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker).
11+
* New method [*graphviz*.**destroy**()](README.md#graphviz_destroy) to remove the Graphviz render from the element it was created on and release any resources it is holding.
12+
913
## [3.0.6]
1014

1115
### Changed

README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Uses [@hpcc-js/wasm](https://github.com/hpcc-systems/hpcc-js-wasm) to do a layou
9595
* [Accessing Elements of the Generated Graph](#accessing-elements-of-the-generated-graph)
9696
* [Accessing the Extracted Data](#accessing-the-extracted-data)
9797
* [Modifying an Existing Graph and Animating the Changes](#modifying-an-existing-graph-and-animating-the-changes)
98+
* [Destroying the Graphviz Renderer](#destroying-the-graphviz-renderer)
9899

99100
### Defining the @hpcc-js/wasm Script Tag
100101

@@ -112,18 +113,18 @@ This will work if a web worker is used and the [*drawNode*](#graphviz_drawNode)
112113

113114
The following table summarizes the recommended script type:
114115

115-
| | *useWorker* = true (default) | *useWorker* = false |
116-
|--------------------------------------------|------------------------------|------------------------|
117-
| <b>*drawNode()/drawEdge()* is not used</b> | javascript/worker | application/javascript |
118-
| <b>*drawNode()/drawEdge()* is used</b> | application/javascript | application/javascript |
116+
| | *useWorker* = true (default) or<br>*useSharedWorker* = true | *useWorker* = false and<br>*useSharedWorker* = false |
117+
|:--|:--|:--|
118+
| <b>*drawNode()/drawEdge()* is not used</b> | javascript/worker | application/javascript |
119+
| <b>*drawNode()/drawEdge()* is used</b> | application/javascript | application/javascript |
119120

120121
### Creating a Graphviz Renderer
121122

122123
#### Creating a Graphviz Renderer on an Existing Selection
123124

124125
<a name="selection_graphviz" href="#selection_graphviz">#</a> <i>selection</i>.<b>graphviz</b>([<i>options</i>]) [<>](https://github.com/magjac/d3-graphviz/blob/master/src/selection/graphviz.js "Source")
125126

126-
Returns a new graphviz renderer instance on the first element in the given *selection*. If a graphviz renderer instance already exists on that element, instead returns the existing graphviz renderer instance. If *options* is specified and is an object, its properties are taken to be options to the graphviz renderer. All options except the *useWorker* option can also be changed later, using individual methods or the [<i>graphviz</i>.<b>options</b>](#graphviz_options) method, see below.
127+
Returns a new graphviz renderer instance on the first element in the given *selection*. If a graphviz renderer instance already exists on that element, instead returns the existing graphviz renderer instance. If *options* is specified and is an object, its properties are taken to be options to the graphviz renderer. All options except the *useWorker* and *useSharedWorker* options can also be changed later, using individual methods or the [<i>graphviz</i>.<b>options</b>](#graphviz_options) method, see below.
127128

128129
##### Supported options
129130

@@ -141,14 +142,15 @@ Returns a new graphviz renderer instance on the first element in the given *sele
141142
| [tweenPrecision](#graphviz_tweenPrecision) | 1 |
142143
| [tweenShapes](#graphviz_tweenShapes) | true |
143144
| useWorker¹ | true |
145+
| useSharedWorker¹ | false |
144146
| [width](#graphviz_width) | null |
145147
| [zoom](#graphviz_zoom) | true |
146148
| [zoomScaleExtent](#graphviz_zoomScaleExtent) | [0.1, 10] |
147149
| [zoomTranslateExtent](#graphviz_zoomTranslateExtent) | [[-∞, -∞], [+∞, +∞]] |
148150

149151
¹ Only has effect when the graphviz renderer instance is created.
150152

151-
If the *useWorker* option is falsey, no web worker is used for the layout stage. The rest of the options are described below. Only the specified options will be changed. The others will keep their current values. If *options* is a boolean it is taken to be the useWorker option (for backwards compatibility).
153+
If the *useSharedWorker* option is truthy, a [shared web worker](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) will be used for the layout stage. Otherwise, if *useWorker* is truthy, a [dedicated web worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) will be used. If both are falsey, no web worker will be used. The rest of the options are described below. Only the specified options will be changed. The others will keep their current values. If *options* is a boolean it is taken to be the useWorker option (for backwards compatibility).
152154

153155
#### Creating a Graphviz Renderer Using a Selector String or a Node
154156

@@ -472,6 +474,12 @@ Removes the node currently drawn with [<i>graphviz</i>.<b>drawNode</b>](#graphvi
472474

473475
Returns a [selection](https://github.com/d3/d3-selection#selection) containing the node currently being drawn. The selection is empty if no node has been drawn or the lastest drawn node has been inserted into the graph data with [<i>graphviz</i>.<b>insertDrawnNode</b>](#graphviz_insertDrawnNode).
474476

477+
### Destroying the Graphviz Renderer
478+
479+
<a name="graphviz_destroy" href="#graphviz_destroy">#</a> <i>graphviz</i>.<b>destroy</b>() [<>](https://github.com/magjac/d3-graphviz/blob/master/src/destroy.js "Source")
480+
481+
Removes the graphviz renderer from the element it was created on, terminates any active decicated web worker and closes any port connected to a shared web worker.
482+
475483
## Examples
476484

477485
* [Basic Example](http://bl.ocks.org/magjac/a23d1f1405c2334f288a9cca4c0ef05b)

src/destroy.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function() {
2+
3+
delete this._selection.node().__graphviz__;
4+
if (this._worker) {
5+
this._workerPortClose();
6+
}
7+
return this;
8+
};

src/dot.js

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,34 +28,49 @@ export function initViz() {
2828
if (this._worker != null) {
2929
var vizURL = this._vizURL;
3030
var graphvizInstance = this;
31-
this._worker.onmessage = function(event) {
31+
this._workerPort.onmessage = function(event) {
3232
var callback = graphvizInstance._workerCallbacks.shift();
33+
callback.call(graphvizInstance, event);
34+
}
35+
if (!vizURL.match(/^https?:\/\/|^\/\//i)) {
36+
// Local URL. Prepend with local domain to be usable in web worker
37+
vizURL = (new window.URL(vizURL, document.location.href)).href;
38+
}
39+
postMessage.call(this, {dot: "", engine: 'dot', vizURL: vizURL}, function(event) {
3340
switch (event.data.type) {
3441
case "init":
3542
graphvizInstance._dispatch.call("initEnd", this);
3643
break;
37-
case "done":
38-
return layoutDone.call(graphvizInstance, event.data.svg, callback);
39-
case "error":
40-
if (graphvizInstance._onerror) {
41-
graphvizInstance._onerror(event.data.error);
42-
} else {
43-
throw event.data.error
44-
}
45-
break;
4644
}
47-
};
48-
if (!vizURL.match(/^https?:\/\/|^\/\//i)) {
49-
// Local URL. Prepend with local domain to be usable in web worker
50-
vizURL = (new window.URL(vizURL, document.location.href)).href;
51-
}
52-
postMessage.call(this, {dot: "", engine: 'dot', vizURL: vizURL});
45+
});
5346
}
5447
}
5548

5649
function postMessage(message, callback) {
5750
this._workerCallbacks.push(callback);
58-
this._worker.postMessage(message);
51+
this._workerPort.postMessage(message);
52+
}
53+
54+
export function layout(src, engine, vizOptions, callback) {
55+
var graphvizInstance = this;
56+
var worker = this._worker;
57+
if (this._worker) {
58+
postMessage.call(this, {
59+
dot: src,
60+
engine: engine,
61+
options: vizOptions,
62+
}, function (event) {
63+
callback.call(this, event.data);
64+
});
65+
} else {
66+
try {
67+
var svgDoc = this.layoutSync(src, "svg", engine, vizOptions);
68+
callback.call(this, {type: 'done', svg: svgDoc});
69+
}
70+
catch(error) {
71+
callback.call(this, {type: 'error', error: error.message});
72+
}
73+
}
5974
}
6075

6176
export default function(src, callback) {
@@ -71,30 +86,25 @@ export default function(src, callback) {
7186
var vizOptions = {
7287
images: images,
7388
};
74-
if (this._worker) {
75-
postMessage.call(this, {
76-
dot: src,
77-
engine: engine,
78-
options: vizOptions,
79-
}, callback);
80-
} else {
81-
if (this.layoutSync == null) {
82-
this._afterInit = this.dot.bind(this, src, callback);
83-
return this;
84-
}
85-
try {
86-
var svgDoc = this.layoutSync(src, "svg", engine, vizOptions);
87-
}
88-
catch(error) {
89+
if (!this._worker && this.layoutSync == null) {
90+
this._afterInit = this.dot.bind(this, src, callback);
91+
return this;
92+
}
93+
this.layout(src, engine, vizOptions, function (data) {
94+
switch (data.type) {
95+
case "error":
8996
if (graphvizInstance._onerror) {
90-
graphvizInstance._onerror(error.message);
91-
return this;
97+
graphvizInstance._onerror(data.error);
9298
} else {
93-
throw error.message
99+
throw data.error.message
94100
}
101+
break;
102+
case "done":
103+
var svgDoc = data.svg;
104+
layoutDone.call(this, svgDoc, callback);
105+
break;
95106
}
96-
layoutDone.call(this, svgDoc, callback);
97-
}
107+
});
98108

99109
return this;
100110
};

src/graphviz.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as d3 from "d3-selection";
22
import {dispatch} from "d3-dispatch";
33
import render from "./render";
4+
import {layout} from "./dot";
45
import dot from "./dot";
56
import data from "./data";
67
import {initViz} from "./dot";
@@ -31,6 +32,7 @@ import {zoomTranslateExtent} from "./zoom";
3132
import on from "./on";
3233
import onerror from "./onerror";
3334
import logEvents from "./logEvents";
35+
import destroy from "./destroy";
3436
import {drawEdge} from "./drawEdge";
3537
import {updateDrawnEdge} from "./drawEdge";
3638
import {moveDrawnEdgeEndPoint} from "./drawEdge";
@@ -44,10 +46,13 @@ import {insertDrawnNode} from "./drawNode";
4446
import {removeDrawnNode} from "./drawNode";
4547
import {drawnNodeSelection} from "./drawNode";
4648
import {workerCode} from "./workerCode";
49+
import {sharedWorkerCode} from "./workerCode";
50+
import {workerCodeBody} from "./workerCode";
4751

4852
export function Graphviz(selection, options) {
4953
this._options = {
5054
useWorker: true,
55+
useSharedWorker: false,
5156
engine: 'dot',
5257
keyMode: 'title',
5358
fade: true,
@@ -72,29 +77,45 @@ export function Graphviz(selection, options) {
7277
this._options.useWorker = options;
7378
}
7479
var useWorker = this._options.useWorker;
80+
var useSharedWorker = this._options.useSharedWorker;
7581
if (typeof Worker == 'undefined') {
7682
useWorker = false;
7783
}
78-
if (useWorker) {
84+
if (typeof SharedWorker == 'undefined') {
85+
useSharedWorker = false;
86+
}
87+
if (useWorker || useSharedWorker) {
7988
var scripts = d3.selectAll('script');
8089
var vizScript = scripts.filter(function() {
8190
return d3.select(this).attr('type') == 'javascript/worker' || (d3.select(this).attr('src') && d3.select(this).attr('src').match(/.*\/@hpcc-js\/wasm/));
8291
});
8392
if (vizScript.size() == 0) {
8493
console.warn('No script tag of type "javascript/worker" was found and "useWorker" is true. Not using web worker.');
8594
useWorker = false;
95+
useSharedWorker = false;
8696
} else {
8797
this._vizURL = vizScript.attr('src');
8898
if (!this._vizURL) {
8999
console.warn('No "src" attribute of was found on the "javascript/worker" script tag and "useWorker" is true. Not using web worker.');
90100
useWorker = false;
101+
useSharedWorker = false;
91102
}
92103
}
93104
}
94-
if (useWorker) {
95-
var blob = new Blob(['(' + workerCode.toString() + ')()']);
105+
if (useSharedWorker) {
106+
const url = 'data:application/javascript;base64,' + btoa(workerCodeBody.toString() + '(' + sharedWorkerCode.toString() + ')()');
107+
this._worker = this._worker = new SharedWorker(url);
108+
this._workerPort = this._worker.port;
109+
this._workerPortClose = this._worker.port.close.bind(this._workerPort);
110+
this._worker.port.start();
111+
this._workerCallbacks = [];
112+
}
113+
else if (useWorker) {
114+
var blob = new Blob([workerCodeBody.toString() + '(' + workerCode.toString() + ')()']);
96115
var blobURL = window.URL.createObjectURL(blob);
97116
this._worker = new Worker(blobURL);
117+
this._workerPort = this._worker;
118+
this._workerPortClose = this._worker.terminate.bind(this._worker);
98119
this._workerCallbacks = [];
99120
}
100121
this._selection = selection;
@@ -155,6 +176,7 @@ Graphviz.prototype = graphviz.prototype = {
155176
zoomScaleExtent: zoomScaleExtent,
156177
zoomTranslateExtent: zoomTranslateExtent,
157178
render: render,
179+
layout: layout,
158180
dot: dot,
159181
data: data,
160182
renderDot: renderDot,
@@ -169,6 +191,7 @@ Graphviz.prototype = graphviz.prototype = {
169191
on: on,
170192
onerror: onerror,
171193
logEvents: logEvents,
194+
destroy: destroy,
172195
drawEdge: drawEdge,
173196
updateDrawnEdge: updateDrawnEdge,
174197
moveDrawnEdgeEndPoint,

src/workerCode.js

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,57 @@
44

55
/* istanbul ignore next */
66

7-
export function workerCode() {
7+
export function workerCodeBody(port) {
8+
89
self.document = {}; // Workaround for "ReferenceError: document is not defined" in hpccWasm
9-
var hpccWasm;
1010

11-
self.onmessage = function(event) {
12-
if (event.data.vizURL) {
11+
port.addEventListener('message', function(event) {
12+
let hpccWasm = self["@hpcc-js/wasm"];
13+
if (hpccWasm == undefined && event.data.vizURL) {
1314
importScripts(event.data.vizURL);
1415
hpccWasm = self["@hpcc-js/wasm"];
1516
hpccWasm.wasmFolder(event.data.vizURL.match(/.*\//)[0]);
16-
// This is an alternative workaround where wasmFolder() is not needed
17-
// document = {currentScript: {src: event.data.vizURL}};
17+
// This is an alternative workaround where wasmFolder() is not needed
18+
// document = {currentScript: {src: event.data.vizURL}};
1819
}
1920
hpccWasm.graphviz.layout(event.data.dot, "svg", event.data.engine, event.data.options).then((svg) => {
2021
if (svg) {
21-
self.postMessage({
22+
port.postMessage({
2223
type: "done",
2324
svg: svg,
2425
});
2526
} else if (event.data.vizURL) {
26-
self.postMessage({
27+
port.postMessage({
2728
type: "init",
2829
});
2930
} else {
30-
self.postMessage({
31+
port.postMessage({
3132
type: "skip",
3233
});
3334
}
3435
}).catch(error => {
35-
self.postMessage({
36+
port.postMessage({
3637
type: "error",
3738
error: error.message,
3839
});
3940
});
41+
});
42+
}
43+
44+
/* istanbul ignore next */
45+
46+
export function workerCode() {
47+
48+
const port = self;
49+
workerCodeBody(port);
50+
}
51+
52+
/* istanbul ignore next */
53+
54+
export function sharedWorkerCode() {
55+
self.onconnect = function(e) {
56+
const port = e.ports[0];
57+
workerCodeBody(port);
58+
port.start();
4059
}
4160
}

0 commit comments

Comments
 (0)