Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/7657_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `Plotly.profile()` API method for performance profiling [[#7657](https://github.com/plotly/plotly.js/pull/7657)]
1 change: 1 addition & 0 deletions draftlogs/XXXX_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add `Plotly.profile()` API method for performance profiling [[#XXXX](https://github.com/plotly/plotly.js/pull/XXXX)]
2 changes: 2 additions & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1413,3 +1413,5 @@ lib.getPositionFromD3Event = function () {
return [d3.event.offsetX, d3.event.offsetY];
}
};

lib.Profiler = require('./profiler');
48 changes: 48 additions & 0 deletions src/lib/profiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

/**
* Profiler utility for collecting render timing data
*
* Usage:
* var profiler = Profiler.start(gd);
* // ... do work ...
* profiler.mark('phaseName');
* // ... do more work ...
* profiler.end();
*/

exports.isEnabled = function(gd) {
return gd && gd._profileEnabled === true;
};

exports.start = function(gd) {
if(!exports.isEnabled(gd)) {
return {
mark: function() {},
end: function() {}
};
}

var startTime = performance.now();
var lastMark = startTime;
var phases = {};

return {
mark: function(phaseName) {
var now = performance.now();
phases[phaseName] = {
duration: now - lastMark,
timestamp: now - startTime
};
lastMark = now;
},
end: function() {
var endTime = performance.now();
return {
total: endTime - startTime,
phases: phases,
timestamp: new Date().toISOString()
};
}
};
};
2 changes: 2 additions & 0 deletions src/plot_api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ exports.downloadImage = require('../snapshot/download');
var templateApi = require('./template_api');
exports.makeTemplate = templateApi.makeTemplate;
exports.validateTemplate = templateApi.validateTemplate;

exports.profile = require('./profile');
42 changes: 39 additions & 3 deletions src/plot_api/plot_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var helpers = require('./helpers');
var subroutines = require('./subroutines');
var editTypes = require('./edit_types');

var Profiler = require('../lib').Profiler;

var AX_NAME_PATTERN = require('../plots/cartesian/constants').AX_NAME_PATTERN;

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

// Start profiler if enabled (returns no-op if disabled)
var profiler = Profiler.start(gd);

if (Lib.isPlainObject(data)) {
var obj = data;
data = obj.data;
Expand Down Expand Up @@ -129,6 +134,7 @@ function _doPlot(gd, data, layout, config) {
}

Plots.supplyDefaults(gd);
profiler.mark('supplyDefaults');

var fullLayout = gd._fullLayout;
var hasCartesian = fullLayout._has('cartesian');
Expand All @@ -145,6 +151,7 @@ function _doPlot(gd, data, layout, config) {
delete fullLayout._shouldCreateBgLayer;
}
}
profiler.mark('makePlotFramework');

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

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

var seq = [Plots.previousPromises, addFrames, drawFramework, marginPushers, marginPushersAgain];
var seq = [
Plots.previousPromises,
addFrames,
drawFramework,
function() { profiler.mark('drawFramework'); },
marginPushers,
function() { profiler.mark('marginPushers'); },
marginPushersAgain,
function() { profiler.mark('marginPushersAgain'); }
];

if (hasCartesian) seq.push(positionAndAutorange);
if (hasCartesian) {
seq.push(
positionAndAutorange,
function() { profiler.mark('positionAndAutorange'); }
);
}

seq.push(subroutines.layoutStyles);
seq.push(
subroutines.layoutStyles,
function() { profiler.mark('layoutStyles'); }
);
if (hasCartesian) {
seq.push(drawAxes, function insideTickLabelsAutorange(gd) {
var insideTickLabelsUpdaterange = gd._fullLayout._insideTickLabelsUpdaterange;
Expand All @@ -367,12 +392,16 @@ function _doPlot(gd, data, layout, config) {
});
}
});
seq.push(function() { profiler.mark('drawAxes'); });
}

seq.push(
subroutines.drawData,
function() { profiler.mark('drawData'); },
subroutines.finalDraw,
function() { profiler.mark('finalDraw'); },
initInteractions,
function() { profiler.mark('initInteractions'); },
Plots.addLinks,
Plots.rehover,
Plots.redrag,
Expand All @@ -391,6 +420,13 @@ function _doPlot(gd, data, layout, config) {
if (!plotDone || !plotDone.then) plotDone = Promise.resolve();

return plotDone.then(function () {
// Finalize profiling and emit event if profiling is enabled
var profileData = profiler.end();
if (profileData && profileData.total) {
gd._profileData = profileData;
Events.triggerHandler(gd, 'plotly_profiled', profileData);
}

emitAfterPlot(gd);
return gd;
});
Expand Down
42 changes: 42 additions & 0 deletions src/plot_api/profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';

var Lib = require('../lib');

/**
* Enable or disable performance profiling on a graph div
*
* @param {string|HTMLDivElement} gd - Graph div or its id
* @param {boolean} [enable=true] - Whether to enable profiling
* @returns {Object|null} - Current profile data if available, null otherwise
*
* Usage:
* Plotly.profile('myDiv'); // Enable profiling
* Plotly.profile('myDiv', true); // Enable profiling
* Plotly.profile('myDiv', false); // Disable profiling
*
* After each render, profile data is available via:
* gd._profileData // Latest profile result
* gd.on('plotly_profiled', function(data) { ... }); // Event listener
*/
function profile(gd, enable) {
gd = Lib.getGraphDiv(gd);

if(!Lib.isPlotDiv(gd)) {
Lib.warn('profile() called on non-plot element');
return null;
}

// Default to enabling
if(enable === undefined) enable = true;

gd._profileEnabled = !!enable;

if(!enable) {
// Clear profile data when disabling
delete gd._profileData;
}

return gd._profileData || null;
}

module.exports = profile;
Loading