Skip to content

Commit 7448e6e

Browse files
committed
Add Plotly.profile() API for performance profiling
1 parent f07f1c7 commit 7448e6e

File tree

7 files changed

+356
-3
lines changed

7 files changed

+356
-3
lines changed

draftlogs/XXXX_add.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add `Plotly.profile()` API method for performance profiling [[#XXXX](https://github.com/plotly/plotly.js/pull/XXXX)]

src/lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,3 +1413,5 @@ lib.getPositionFromD3Event = function () {
14131413
return [d3.event.offsetX, d3.event.offsetY];
14141414
}
14151415
};
1416+
1417+
lib.Profiler = require('./profiler');

src/lib/profiler.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
/**
4+
* Profiler utility for collecting render timing data
5+
*
6+
* Usage:
7+
* var profiler = Profiler.start(gd);
8+
* // ... do work ...
9+
* profiler.mark('phaseName');
10+
* // ... do more work ...
11+
* profiler.end();
12+
*/
13+
14+
exports.isEnabled = function(gd) {
15+
return gd && gd._profileEnabled === true;
16+
};
17+
18+
exports.start = function(gd) {
19+
if(!exports.isEnabled(gd)) {
20+
return {
21+
mark: function() {},
22+
end: function() {}
23+
};
24+
}
25+
26+
var startTime = performance.now();
27+
var lastMark = startTime;
28+
var phases = {};
29+
30+
return {
31+
mark: function(phaseName) {
32+
var now = performance.now();
33+
phases[phaseName] = {
34+
duration: now - lastMark,
35+
timestamp: now - startTime
36+
};
37+
lastMark = now;
38+
},
39+
end: function() {
40+
var endTime = performance.now();
41+
return {
42+
total: endTime - startTime,
43+
phases: phases,
44+
timestamp: new Date().toISOString()
45+
};
46+
}
47+
};
48+
};

src/plot_api/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ exports.downloadImage = require('../snapshot/download');
3737
var templateApi = require('./template_api');
3838
exports.makeTemplate = templateApi.makeTemplate;
3939
exports.validateTemplate = templateApi.validateTemplate;
40+
41+
exports.profile = require('./profile');

src/plot_api/plot_api.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ var helpers = require('./helpers');
3030
var subroutines = require('./subroutines');
3131
var editTypes = require('./edit_types');
3232

33+
var Profiler = require('../lib').Profiler;
34+
3335
var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN;
3436

3537
var numericNameWarningCount = 0;
@@ -64,6 +66,9 @@ function _doPlot(gd, data, layout, config) {
6466
// Events.init is idempotent and bails early if gd has already been init'd
6567
Events.init(gd);
6668

69+
// Start profiler if enabled (returns no-op if disabled)
70+
var profiler = Profiler.start(gd);
71+
6772
if (Lib.isPlainObject(data)) {
6873
var obj = data;
6974
data = obj.data;
@@ -129,6 +134,7 @@ function _doPlot(gd, data, layout, config) {
129134
}
130135

131136
Plots.supplyDefaults(gd);
137+
profiler.mark('supplyDefaults');
132138

133139
var fullLayout = gd._fullLayout;
134140
var hasCartesian = fullLayout._has('cartesian');
@@ -145,6 +151,7 @@ function _doPlot(gd, data, layout, config) {
145151
delete fullLayout._shouldCreateBgLayer;
146152
}
147153
}
154+
profiler.mark('makePlotFramework');
148155

149156
// clear gradient and pattern defs on each .plot call, because we know we'll loop through all traces
150157
Drawing.initGradients(gd);
@@ -159,6 +166,7 @@ function _doPlot(gd, data, layout, config) {
159166
// to force redoing calcdata, just delete it before calling _doPlot
160167
var recalc = !gd.calcdata || gd.calcdata.length !== (gd._fullData || []).length;
161168
if (recalc) Plots.doCalcdata(gd);
169+
profiler.mark('doCalcdata');
162170

163171
// in case it has changed, attach fullData traces to calcdata
164172
for (var i = 0; i < gd.calcdata.length; i++) {
@@ -351,11 +359,28 @@ function _doPlot(gd, data, layout, config) {
351359
return Axes.draw(gd, graphWasEmpty ? '' : 'redraw');
352360
}
353361

354-
var seq = [Plots.previousPromises, addFrames, drawFramework, marginPushers, marginPushersAgain];
362+
var seq = [
363+
Plots.previousPromises,
364+
addFrames,
365+
drawFramework,
366+
function() { profiler.mark('drawFramework'); },
367+
marginPushers,
368+
function() { profiler.mark('marginPushers'); },
369+
marginPushersAgain,
370+
function() { profiler.mark('marginPushersAgain'); }
371+
];
355372

356-
if (hasCartesian) seq.push(positionAndAutorange);
373+
if (hasCartesian) {
374+
seq.push(
375+
positionAndAutorange,
376+
function() { profiler.mark('positionAndAutorange'); }
377+
);
378+
}
357379

358-
seq.push(subroutines.layoutStyles);
380+
seq.push(
381+
subroutines.layoutStyles,
382+
function() { profiler.mark('layoutStyles'); }
383+
);
359384
if (hasCartesian) {
360385
seq.push(drawAxes, function insideTickLabelsAutorange(gd) {
361386
var insideTickLabelsUpdaterange = gd._fullLayout._insideTickLabelsUpdaterange;
@@ -367,12 +392,16 @@ function _doPlot(gd, data, layout, config) {
367392
});
368393
}
369394
});
395+
seq.push(function() { profiler.mark('drawAxes'); });
370396
}
371397

372398
seq.push(
373399
subroutines.drawData,
400+
function() { profiler.mark('drawData'); },
374401
subroutines.finalDraw,
402+
function() { profiler.mark('finalDraw'); },
375403
initInteractions,
404+
function() { profiler.mark('initInteractions'); },
376405
Plots.addLinks,
377406
Plots.rehover,
378407
Plots.redrag,
@@ -391,6 +420,13 @@ function _doPlot(gd, data, layout, config) {
391420
if (!plotDone || !plotDone.then) plotDone = Promise.resolve();
392421

393422
return plotDone.then(function () {
423+
// Finalize profiling and emit event if profiling is enabled
424+
var profileData = profiler.end();
425+
if (profileData && profileData.total) {
426+
gd._profileData = profileData;
427+
Events.triggerHandler(gd, 'plotly_profiled', profileData);
428+
}
429+
394430
emitAfterPlot(gd);
395431
return gd;
396432
});

src/plot_api/profile.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use strict';
2+
3+
var Lib = require('../lib');
4+
5+
/**
6+
* Enable or disable performance profiling on a graph div
7+
*
8+
* @param {string|HTMLDivElement} gd - Graph div or its id
9+
* @param {boolean} [enable=true] - Whether to enable profiling
10+
* @returns {Object|null} - Current profile data if available, null otherwise
11+
*
12+
* Usage:
13+
* Plotly.profile('myDiv'); // Enable profiling
14+
* Plotly.profile('myDiv', true); // Enable profiling
15+
* Plotly.profile('myDiv', false); // Disable profiling
16+
*
17+
* After each render, profile data is available via:
18+
* gd._profileData // Latest profile result
19+
* gd.on('plotly_profiled', function(data) { ... }); // Event listener
20+
*/
21+
function profile(gd, enable) {
22+
gd = Lib.getGraphDiv(gd);
23+
24+
if(!Lib.isPlotDiv(gd)) {
25+
Lib.warn('profile() called on non-plot element');
26+
return null;
27+
}
28+
29+
// Default to enabling
30+
if(enable === undefined) enable = true;
31+
32+
gd._profileEnabled = !!enable;
33+
34+
if(!enable) {
35+
// Clear profile data when disabling
36+
delete gd._profileData;
37+
}
38+
39+
return gd._profileData || null;
40+
}
41+
42+
module.exports = profile;

0 commit comments

Comments
 (0)