Skip to content

Commit 85e8770

Browse files
authored
Merge pull request #1772 from plotly/misc-perf
Misc perf improvements
2 parents a6db253 + 0129d5a commit 85e8770

File tree

32 files changed

+527
-421
lines changed

32 files changed

+527
-421
lines changed

devtools/test_dashboard/devtools.js

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ var credentials = require('../../build/credentials.json');
88
var Lib = require('@src/lib');
99
var d3 = Plotly.d3;
1010

11+
require('./perf');
12+
1113
// Our gracious testing object
1214
var Tabs = {
1315

devtools/test_dashboard/perf.js

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
/*
4+
* timeit: tool for performance testing
5+
* f: function to be tested
6+
* n: number of timing runs
7+
* nchunk: optional number of repetitions per timing run - useful if
8+
* the function is very fast. Note though that if arg is a function
9+
* it will not be re-evaluated within the chunk, only before each chunk.
10+
* arg: optional argument to the function. Can be a function itself
11+
* to provide a changing input to f
12+
*/
13+
window.timeit = function(f, n, nchunk, arg) {
14+
var times = new Array(n);
15+
var totalTime = 0;
16+
var _arg;
17+
var t0, t1, dt;
18+
19+
for(var i = 0; i < n; i++) {
20+
if(typeof arg === 'function') _arg = arg();
21+
else _arg = arg;
22+
23+
if(nchunk) {
24+
t0 = performance.now();
25+
for(var j = 0; j < nchunk; j++) { f(_arg); }
26+
t1 = performance.now();
27+
dt = (t1 - t0) / nchunk;
28+
}
29+
else {
30+
t0 = performance.now();
31+
f(_arg);
32+
t1 = performance.now();
33+
dt = t1 - t0;
34+
}
35+
36+
times[i] = dt;
37+
totalTime += dt;
38+
}
39+
40+
var first = (times[0]).toFixed(4);
41+
var last = (times[n - 1]).toFixed(4);
42+
times.sort();
43+
var min = (times[0]).toFixed(4);
44+
var max = (times[n - 1]).toFixed(4);
45+
var median = (times[Math.ceil(n / 2)]).toFixed(4);
46+
var mean = (totalTime / n).toFixed(4);
47+
console.log((f.name || 'function') + ' timing (ms) - min: ' + min +
48+
' max: ' + max +
49+
' median: ' + median +
50+
' mean: ' + mean +
51+
' first: ' + first +
52+
' last: ' + last
53+
);
54+
};

src/components/annotations/draw.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
168168
fontColor: hoverFont.color
169169
}, {
170170
container: fullLayout._hoverlayer.node(),
171-
outerContainer: fullLayout._paper.node()
171+
outerContainer: fullLayout._paper.node(),
172+
gd: gd
172173
});
173174
})
174175
.on('mouseout', function() {
@@ -214,7 +215,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
214215
}[options.align] || 'middle'
215216
});
216217

217-
svgTextUtils.convertToTspans(s, drawGraphicalElements);
218+
svgTextUtils.convertToTspans(s, gd, drawGraphicalElements);
218219
return s;
219220
}
220221

@@ -554,6 +555,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
554555
// (head/tail/text) all together
555556
dragElement.init({
556557
element: arrowDrag.node(),
558+
gd: gd,
557559
prepFn: function() {
558560
var pos = Drawing.getTranslate(annTextGroupInner);
559561

@@ -616,6 +618,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
616618
// textbox and tail, leave the head untouched
617619
dragElement.init({
618620
element: annTextGroupInner.node(),
621+
gd: gd,
619622
prepFn: function() {
620623
baseTextTransform = annTextGroup.attr('transform');
621624
update = {};
@@ -686,7 +689,7 @@ function drawRaw(gd, options, index, subplotId, xa, ya) {
686689
}
687690

688691
if(gd._context.editable) {
689-
annText.call(svgTextUtils.makeEditable, annTextGroupInner)
692+
annText.call(svgTextUtils.makeEditable, {delegate: annTextGroupInner, gd: gd})
690693
.call(textLayout)
691694
.on('edit', function(_text) {
692695
options.text = _text;

src/components/colorbar/draw.js

+1
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ module.exports = function draw(gd, id) {
556556

557557
dragElement.init({
558558
element: container.node(),
559+
gd: gd,
559560
prepFn: function() {
560561
t0 = container.attr('transform');
561562
setCursor(container);

src/components/dragelement/index.js

+11-18
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ dragElement.unhoverRaw = unhover.raw;
2626

2727
/**
2828
* Abstracts click & drag interactions
29+
*
30+
* During the interaction, a "coverSlip" element - a transparent
31+
* div covering the whole page - is created, which has two key effects:
32+
* - Lets you drag beyond the boundaries of the plot itself without
33+
* dropping (but if you drag all the way out of the browser window the
34+
* interaction will end)
35+
* - Freezes the cursor: whatever mouse cursor the drag element had when the
36+
* interaction started gets copied to the coverSlip for use until mouseup
37+
*
2938
* @param {object} options with keys:
3039
* element (required) the DOM element to drag
3140
* prepFn (optional) function(event, startX, startY)
@@ -44,28 +53,20 @@ dragElement.unhoverRaw = unhover.raw;
4453
* numClicks is how many clicks we've registered within
4554
* a doubleclick time
4655
* e is the original event
47-
* setCursor (optional) function(event)
48-
* executed on mousemove before mousedown
49-
* the purpose of this callback is to update the mouse cursor before
50-
* the click & drag interaction has been initiated
5156
*/
5257
dragElement.init = function init(options) {
53-
var gd = Lib.getPlotDiv(options.element) || {},
58+
var gd = options.gd,
5459
numClicks = 1,
5560
DBLCLICKDELAY = interactConstants.DBLCLICKDELAY,
5661
startX,
5762
startY,
5863
newMouseDownTime,
5964
dragCover,
60-
initialTarget,
61-
initialOnMouseMove;
65+
initialTarget;
6266

6367
if(!gd._mouseDownTime) gd._mouseDownTime = 0;
6468

6569
function onStart(e) {
66-
// disable call to options.setCursor(evt)
67-
options.element.onmousemove = initialOnMouseMove;
68-
6970
// make dragging and dragged into properties of gd
7071
// so that others can look at and modify them
7172
gd._dragged = false;
@@ -116,10 +117,6 @@ dragElement.init = function init(options) {
116117
}
117118

118119
function onDone(e) {
119-
// re-enable call to options.setCursor(evt)
120-
initialOnMouseMove = options.element.onmousemove;
121-
if(options.setCursor) options.element.onmousemove = options.setCursor;
122-
123120
dragCover.onmousemove = null;
124121
dragCover.onmouseup = null;
125122
dragCover.onmouseout = null;
@@ -166,10 +163,6 @@ dragElement.init = function init(options) {
166163
return Lib.pauseEvent(e);
167164
}
168165

169-
// enable call to options.setCursor(evt)
170-
initialOnMouseMove = options.element.onmousemove;
171-
if(options.setCursor) options.element.onmousemove = options.setCursor;
172-
173166
options.element.onmousedown = onStart;
174167
options.element.style.pointerEvents = 'all';
175168
};

src/components/drawing/index.js

+37-16
Original file line numberDiff line numberDiff line change
@@ -392,15 +392,14 @@ drawing.singlePointStyle = function(d, sel, trace, markerScale, lineScale, gd) {
392392

393393
};
394394

395-
drawing.pointStyle = function(s, trace) {
395+
drawing.pointStyle = function(s, trace, gd) {
396396
if(!s.size()) return;
397397

398398
// allow array marker and marker line colors to be
399399
// scaled by given max and min to colorscales
400400
var marker = trace.marker;
401401
var markerScale = drawing.tryColorscale(marker, '');
402402
var lineScale = drawing.tryColorscale(marker, 'line');
403-
var gd = Lib.getPlotDiv(s.node());
404403

405404
s.each(function(d) {
406405
drawing.singlePointStyle(d, d3.select(this), trace, markerScale, lineScale, gd);
@@ -423,7 +422,7 @@ drawing.tryColorscale = function(marker, prefix) {
423422
// draw text at points
424423
var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1},
425424
LINEEXPAND = 1.3;
426-
drawing.textPointStyle = function(s, trace) {
425+
drawing.textPointStyle = function(s, trace, gd) {
427426
s.each(function(d) {
428427
var p = d3.select(this),
429428
text = d.tx || trace.text;
@@ -454,7 +453,7 @@ drawing.textPointStyle = function(s, trace) {
454453
d.tc || trace.textfont.color)
455454
.attr('text-anchor', h)
456455
.text(text)
457-
.call(svgTextUtils.convertToTspans);
456+
.call(svgTextUtils.convertToTspans, gd);
458457
var pgroup = d3.select(this.parentNode),
459458
tspans = p.selectAll('tspan.line'),
460459
numLines = ((tspans[0].length || 1) - 1) * LINEEXPAND + 1,
@@ -611,19 +610,18 @@ drawing.makeTester = function() {
611610
// in a reference frame where it isn't translated and its anchor
612611
// point is at (0,0)
613612
// always returns a copy of the bbox, so the caller can modify it safely
614-
var savedBBoxes = [];
613+
drawing.savedBBoxes = {};
614+
var savedBBoxesCount = 0;
615615
var maxSavedBBoxes = 10000;
616616

617617
drawing.bBox = function(node) {
618618
// cache elements we've already measured so we don't have to
619619
// remeasure the same thing many times
620-
var saveNum = node.attributes['data-bb'];
621-
if(saveNum && saveNum.value) {
622-
return Lib.extendFlat({}, savedBBoxes[saveNum.value]);
623-
}
620+
var hash = nodeHash(node);
621+
var out = drawing.savedBBoxes[hash];
622+
if(out) return Lib.extendFlat({}, out);
624623

625-
var tester3 = drawing.tester;
626-
var tester = tester3.node();
624+
var tester = drawing.tester.node();
627625

628626
// copy the node to test into the tester
629627
var testNode = node.cloneNode(true);
@@ -655,18 +653,41 @@ drawing.bBox = function(node) {
655653
// make sure we don't have too many saved boxes,
656654
// or a long session could overload on memory
657655
// by saving boxes for long-gone elements
658-
if(savedBBoxes.length >= maxSavedBBoxes) {
659-
d3.selectAll('[data-bb]').attr('data-bb', null);
660-
savedBBoxes = [];
656+
if(savedBBoxesCount >= maxSavedBBoxes) {
657+
drawing.savedBBoxes = {};
658+
maxSavedBBoxes = 0;
661659
}
662660

663661
// cache this bbox
664-
node.setAttribute('data-bb', savedBBoxes.length);
665-
savedBBoxes.push(bb);
662+
drawing.savedBBoxes[hash] = bb;
663+
savedBBoxesCount++;
666664

667665
return Lib.extendFlat({}, bb);
668666
};
669667

668+
// capture everything about a node (at least in our usage) that
669+
// impacts its bounding box, given that bBox clears x, y, and transform
670+
// TODO: is this really everything? Is it worth taking only parts of style,
671+
// so we can share across more changes (like colors)? I guess we can't strip
672+
// colors and stuff from inside innerHTML so maybe not worth bothering outside.
673+
// TODO # 2: this can be long, so could take a lot of memory, do we want to
674+
// hash it? But that can be slow...
675+
// extracting this string from a typical element takes ~3 microsec, where
676+
// doing a simple hash ala https://stackoverflow.com/questions/7616461
677+
// adds ~15 microsec (nearly all of this is spent in charCodeAt)
678+
// function hash(s) {
679+
// var h = 0;
680+
// for (var i = 0; i < s.length; i++) {
681+
// h = (((h << 5) - h) + s.charCodeAt(i)) | 0; // codePointAt?
682+
// }
683+
// return h;
684+
// }
685+
function nodeHash(node) {
686+
return node.innerHTML +
687+
node.getAttribute('text-anchor') +
688+
node.getAttribute('style');
689+
}
690+
670691
/*
671692
* make a robust clipPath url from a local id
672693
* note! We'd better not be exporting from a page

src/components/fx/hover.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ exports.loneHover = function loneHover(hoverItem, opts) {
161161
outerContainer: outerContainer3
162162
};
163163

164-
var hoverLabel = createHoverText([pointData], fullOpts);
164+
var hoverLabel = createHoverText([pointData], fullOpts, opts.gd);
165165
alignHoverText(hoverLabel, fullOpts.rotateLabels);
166166

167167
return hoverLabel.node();
@@ -490,7 +490,7 @@ function _hover(gd, evt, subplot) {
490490
commonLabelOpts: fullLayout.hoverlabel
491491
};
492492

493-
var hoverLabels = createHoverText(hoverData, labelOpts);
493+
var hoverLabels = createHoverText(hoverData, labelOpts, gd);
494494

495495
hoverAvoidOverlaps(hoverData, rotateLabels ? 'xa' : 'ya');
496496

@@ -523,7 +523,7 @@ function _hover(gd, evt, subplot) {
523523
});
524524
}
525525

526-
function createHoverText(hoverData, opts) {
526+
function createHoverText(hoverData, opts, gd) {
527527
var hovermode = opts.hovermode;
528528
var rotateLabels = opts.rotateLabels;
529529
var bgColor = opts.bgColor;
@@ -595,7 +595,7 @@ function createHoverText(hoverData, opts) {
595595
.attr('data-notex', 1);
596596

597597
ltext.text(t0)
598-
.call(svgTextUtils.convertToTspans)
598+
.call(svgTextUtils.convertToTspans, gd)
599599
.call(Drawing.setPosition, 0, 0)
600600
.selectAll('tspan.line')
601601
.call(Drawing.setPosition, 0, 0);
@@ -745,7 +745,7 @@ function createHoverText(hoverData, opts) {
745745
.call(Drawing.setPosition, 0, 0)
746746
.text(text)
747747
.attr('data-notex', 1)
748-
.call(svgTextUtils.convertToTspans);
748+
.call(svgTextUtils.convertToTspans, gd);
749749
tx.selectAll('tspan.line')
750750
.call(Drawing.setPosition, 0, 0);
751751

@@ -761,7 +761,7 @@ function createHoverText(hoverData, opts) {
761761
.text(name)
762762
.call(Drawing.setPosition, 0, 0)
763763
.attr('data-notex', 1)
764-
.call(svgTextUtils.convertToTspans);
764+
.call(svgTextUtils.convertToTspans, gd);
765765
tx2.selectAll('tspan.line')
766766
.call(Drawing.setPosition, 0, 0);
767767
tx2width = tx2.node().getBoundingClientRect().width + 2 * HOVERTEXTPAD;

src/components/legend/draw.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ module.exports = function draw(gd) {
111111
traces.enter().append('g').attr('class', 'traces');
112112
traces.exit().remove();
113113

114-
traces.call(style)
114+
traces.call(style, gd)
115115
.style('opacity', function(d) {
116116
var trace = d[0].trace;
117117
if(Registry.traceIs(trace, 'pie')) {
@@ -317,6 +317,7 @@ module.exports = function draw(gd) {
317317

318318
dragElement.init({
319319
element: legend.node(),
320+
gd: gd,
320321
prepFn: function() {
321322
var transform = Drawing.getTranslate(legend);
322323

@@ -380,14 +381,14 @@ function drawTexts(g, gd) {
380381
.text(name);
381382

382383
function textLayout(s) {
383-
svgTextUtils.convertToTspans(s, function() {
384+
svgTextUtils.convertToTspans(s, gd, function() {
384385
s.selectAll('tspan.line').attr({x: s.attr('x')});
385386
g.call(computeTextDimensions, gd);
386387
});
387388
}
388389

389390
if(gd._context.editable && !isPie) {
390-
text.call(svgTextUtils.makeEditable)
391+
text.call(svgTextUtils.makeEditable, {gd: gd})
391392
.call(textLayout)
392393
.on('edit', function(text) {
393394
this.attr({'data-unformatted': text});

0 commit comments

Comments
 (0)