Skip to content

Misc perf improvements #1772

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 11, 2017
Merged

Misc perf improvements #1772

merged 14 commits into from
Jun 11, 2017

Conversation

alexcjohnson
Copy link
Collaborator

A bunch of perf improvements. The biggest one is changing how we cache Drawing.bBox results ea883e6 - instead of having an element keep track of its own caching, I changed it to just using the contents of the node (its innerHTML plus its style and text-anchor - I'm 90% sure this is sufficient to determine the bbox but needs a bit more testing). This means we're not always invalidating the cache, whether accidentally or out of an abundance of caution, and in fact even unrelated elements that have the same contents and styling can use the same bbox (such as matching labels on different axes).

Another fairly significant one is removing an unnecessary double-draw of ticks and grids during Plotly.plot b4a2f4d - no idea where that came from but it doesn't seem to cause any problems to pare that down.

Two smaller ones:

  • don't draw placeholder titles that we're just going to delete c02204f
  • remove Lib.getPlotDiv 032cf7c - this was by far the most intrusive change (I had to pass gd around to a bunch of places that didn't have it before), for the smallest benefit, but it turns out instanceof HTMLElement (which is actually in Lib.isPlotDiv which I didn't get rid of... but that one calls it once and getPlotDiv called it in a loop) could be kind of slow.

Also I added a perf tester timeit to the global scope in the test dashboard, just for convenience.

I'd like to get #1767 merged before finishing this so I can bring the test improvements in here.

@@ -481,13 +481,6 @@ lib.containsAny = function(s, fragments) {
return false;
};

// get the parent Plotly plot of any element. Whoo jquery-free tree climbing!
lib.getPlotDiv = function(el) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful commit.

Note that Lib.isPlotDiv is currently used in W2. No such problem for Lib.getPlotDiv.

@@ -83,7 +89,7 @@ Titles.draw = function(gd, titleClass, options) {
}

var el = group.selectAll('text')
.data([0]);
.data(elShouldExist ? [0] : []);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 🍺

@@ -8,6 +8,8 @@ var credentials = require('../../build/credentials.json');
var Lib = require('@src/lib');
var d3 = Plotly.d3;

require('./perf');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @rreusser would be nice to have something like that in https://github.com/rreusser/plotly-mock-viewer

Moreover, @dfcreative's brilliant ✨ https://www.npmjs.com/package/fps-indicator ✨ would be a nice addition to our dev tools.

// }
// return h;
// }
function nodeHash(node) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting patch. Do you have any benchmark results comparing old and new Drawing.bBox?

If you're concerned about memory usage, you might want to consider setting maxSavedBBoxes to something less than 1e4. 10000 text nodes on one page sounds a bit much.

cc'ing @monfera @rreusser who might have experience doing stuff like this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any benchmark results comparing old and new Drawing.bBox?

I don't have any explicit benchmarks, but putting in a few console.log statements you can see that previously during redraws (and especially interactions like dragging axis ends) we were only actually using the cache in a few cases, mostly we were redoing the cloneNode and getBoundingClientRect. Now we hardly ever need to do that. The key is that managing data-bb so we can safely use the cache is really hard and we weren't doing it well. This way there's nothing to manage, and nodeHash is very fast (as long as I don't actually hash the resulting string 🤔 ).

10000 text nodes on one page sounds a bit much.

Yes, perhaps I could drop the limit a bit, but two things to note:

  • I'm not storing text nodes, just a lookup object of {nodeHash: bBox} where bBox is a plain object, my main concern is that nodeHash itself can be quite a long string, though I guess a bBox object of 6 numbers can also take non-negligible memory.
  • A large number of text nodes is exactly when this would be most useful, and wiping the cache will kill this whole gain. Perhaps I could take advantage of Object.keys being ordered in insertion order (usually? always?) and drop the oldest 50% of cache entries or something if we hit the limit?

@monfera
Copy link
Contributor

monfera commented Jun 9, 2017

Looks great!

  • I've often seen double or worse rendering in imperative code, hard to automatically catch, I guess this double render was not intended
  • bounding box calc is in good part expensive because the layouting must be synchronously awaited, so I'd guess avoiding bbox calc on identical tick text is even bigger than avoiding repeat calls
  • caching is best if global, but risk is mem leak

return function() {
gd._mouseDownTime = 0; // ensure independence from any previous clicks
return doubleClick(getDragger(subplot, directions));
};
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man, this one was brutal - the key was that (locally, in chrome, when running multiple tests) it was seeing a previous click (from the same test, despite the fact that it still cared whether another test had been run before this one) too close to the first click of the doubleclick, and ended up responding as if there were two doubleclicks - which I still don't quite understand, I didn't think a triple-click would look like two doubles, but anyway this works and we can use this in the future (similar to your trick with gd._lastHoverTime @etpinard ) to cut down on unnecessary delays in the test suite.

For future reference @etpinard since we talked about potentially doing this, I went off on a dead end for a while when I noticed that calling Plotly.purge(gd) during destroyGraphDiv caused all sorts of problems... That turns out to be unrelated, that sometimes there are some things still waiting to happen on the plot after tests have finished (like rehover) which will fail (and confusingly attribute the failure to the next test that's run, since it's happening async) if we've purged the plot of attributes like emit but they still complete without failure if all we've done is remove the gd from the DOM.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing down the details.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

🎉 nice work

The only reason we needed two initInteractions during Plotly.plot
is so the second one could pick up the `onmousemove` from the first
to cache as `initialMouseMove`, but it works fine now to remove that
junk entirely.
@alexcjohnson
Copy link
Collaborator Author

Some profiling on small charts: All results from Chrome on my mac with no profiles running. Running profiles adds ~5% to these times with no screenshots, ~10% with screenshots (interestingly the profiling penalty seems bigger even fractionally before the change). All results are averaging ~10 runs of Plotly.newPlot on the same data repeatedly, throwing away the first one. This gives extra weight to the caching improvements, but as my interest is mainly in interactivity performance that's what I want.

using mock category_dtick_3 (small line chart with a linear and a category axis):

  • before this branch: 144ms
  • after this branch: 63ms

using a chart with ~30 variable-size bubbles, gradient fill, long category names:

  • before this branch: 444ms
  • after this branch: 270ms

@alexcjohnson
Copy link
Collaborator Author

(offline weekend 💃 from @etpinard )

@alexcjohnson alexcjohnson merged commit 85e8770 into master Jun 11, 2017
@alexcjohnson alexcjohnson deleted the misc-perf branch June 11, 2017 16:07
@@ -141,6 +140,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index) {

dragElement.init(dragOptions);

shapePath.node().onmousemove = updateDragMode;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Referencing the ticket for annotation edits -> #679

@@ -57,15 +62,11 @@ dragElement.init = function init(options) {
startY,
newMouseDownTime,
dragCover,
initialTarget,
initialOnMouseMove;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing. Thanks for 🔪 that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants