Skip to content

Instantly share code, notes, and snippets.

@omrilotan
Last active March 30, 2019 12:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save omrilotan/e31d97dea6600698ab9a8fe1e253512e to your computer and use it in GitHub Desktop.
Save omrilotan/e31d97dea6600698ab9a8fe1e253512e to your computer and use it in GitHub Desktop.
Monitor performance of injected scripts (execution time)

Measure execution time of scripts injected to browser runtime

We override (monkey-patch) native Node insertion to the DOM, and wrap its content with performance marks. Later we send the duration of each execution along with the source domain of the script.

(function() {
/**
* These functions will get "monkey-patched"
* @type {[String]}
*/
const FUNCTIONS = ['appendChild', 'insertBefore'];
/**
* A random string to prevent accidental event override
* @type {String}
*/
const PREFIX = Math.random().toString(36).substring(2);
/**
* Prevent collisions of multiple scripts from same domain by numbering them
* @type {Number}
*/
let counter = 1;
/**
* Wrap text content of a DOM node
* @param {String} report
* @param {Node} element
* @return {undefined}
*/
function wrap(report, element) {
try {
const { tagName, textContent } = element;
// Escape if the element is not a script tag or has no text content (e.g. <script src=)
if (!tagName || !textContent || tagName.toLowerCase() !== 'script') { return; }
/**
* Create a stack to retreive the origin of unattributed script
* @type {String}
*/
const { stack = '' } = new Error();
/**
* Extract source domain from the last line of the stack
* @type {String}
*/
const [,source] = stack.split('\n').pop().match(/\/\/([\w\d\.]*)\//) || [,'unknown'];
/**
* A unique tag
* @type {String}
*/
const tag = PREFIX + counter++;
/**
* Unique tags for "start" and "end" events
* @type {String}
*/
const [start, end] = ['a', 'b'].map(n => [tag, n].join('_'));
// Wrap original content with measuring
element.textContent = [
`performance.mark('${start}');`,
// Original text context (code)
textContent,
`performance.mark('${end}');`,
`performance.measure('${tag}', '${start}', '${end}');`,
// Send measurement to "report" function
'(function([{duration} = {}]) {',
` ${report}({source: '${source}', duration});`,
`})(performance.getEntriesByName('${tag}'));`,
// Cleanup
`performance.clearMarks('${start}');`,
`performance.clearMarks('${end}');`,
`performance.clearMeasures('${tag}');`,
].join('\n');
} catch (e) {
// do nothing
}
}
/**
* Wrap injected content with a measuring mechanism
* @param {String} report Global method name (supports dot notation)
* @return {undefined}
*/
function monitorInjection(report = 'console.log') {
if (!window.hasOwnProperty('performance')) { return; }
// Monkey-patch each of these functions
FUNCTIONS.forEach(
/**
* Monkey-patch the function with this name on Node prototype
* @param {String} fn
* @return {undefined}
*/
fn => {
const original = Node.prototype[fn];
/**
* Monkey-patched node functionality
* @param {...Any} args Original arguments
* @return {Any} Original response
*/
Node.prototype[fn] = function(...args) {
wrap(report, ...args);
// Execute and return original functionality
return original.apply(this, args);
};
}
);
}
// Register
monitorInjection(/* put alternative global report method here (string) */);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment