cpupro
Version:
Rethinking of CPU profile (collected in Node.js or Chromium browsers) analysis
165 lines (138 loc) • 4.65 kB
JavaScript
/* eslint-env node */
const fs = require('fs');
const { Readable } = require('stream');
const jsonExt = require('@discoveryjs/json-ext');
const v8profiler = require('v8-profiler-next');
const { defaultFilename } = require('./utils');
const createReport = require('./report');
const jsonxl = require('./tmp/jsonxl-snapshot9');
const packageName = require('../package.json').name;
const profileMeta = new Map();
const profiles = new WeakMap();
function v8profileName(name) {
return `${packageName}:${name || 'profile'}`;
}
function assertProfileName(name) {
if (!profileMeta.has(name || '')) {
throw new Error(`No started CPU profile ${name ? `with name "${name}" ` : ''}is found`);
}
}
function profile(name) {
// v8profiler.setSamplingInterval(10);
if (profileMeta.has(name || '')) {
return profileMeta.get(name || '').capture;
}
const capture = {
mark(name) {
meta.marks.push({
timestamp: performance.now(),
name
});
return capture;
},
profileEnd() {
return profiles.get(capture) || profileEnd(name);
},
onEnd(callback) {
meta.onEndCallbacks.push(callback);
return capture;
},
...Object.fromEntries([
'writeToFile',
'writeJsonxlToFile',
'writeReport',
'openReport'
].map((method) => [method, (...args) => {
meta.onEndCallbacks.push(result => result[method](...args));
return capture;
}]))
};
const meta = {
capture,
startTime: Date.now(),
markTime: null,
marks: [],
onEndCallbacks: []
};
profileMeta.set(name || '', meta);
process.on('exit', capture.profileEnd);
v8profiler.startProfiling(v8profileName(name), true);
meta.markTime = performance.now();
return capture;
}
function mark(name, profileName) {
assertProfileName(profileName);
const { profile } = profileMeta.get(profileName || '');
profile.mark(name);
}
function profileEnd(name) {
assertProfileName(name);
// set generateType 1 to generate a format for cpuprofile
// to be compatible with most modern devtools
v8profiler.setGenerateType(1);
const data = v8profiler.stopProfiling(v8profileName(name));
const { capture, startTime, marks, onEndCallbacks } = profileMeta.get(name || '');
let report = null;
process.off('exit', capture.profileEnd);
profileMeta.delete(name || '');
data.delete();
delete data.delete;
// normalize marks timestamps
data.x_marks = marks;
for (const mark of marks) {
mark.timestamp = data.startTime + Math.round(1000 * (mark.timestamp - startTime));
}
function writeFileAction(destFilename, fn) {
fn(destFilename);
return destFilename;
}
function getReport() {
return report || (report = createReport(data));
}
const result = Object.freeze({
data,
writeToFileAsync(filepath) {
return writeFileAction(filepath || defaultFilename('cpuprofile'), (destFilename) => {
Readable.from(jsonExt.stringifyChunked(data))
.pipe(fs.createWriteStream(destFilename));
});
},
writeToFile(filepath) {
return writeFileAction(filepath || defaultFilename('cpuprofile'), (destFilename) => {
const fd = fs.openSync(destFilename, 'w');
for (const chunk of jsonExt.stringifyChunked(data)) {
fs.writeFileSync(fd, chunk);
}
fs.closeSync(fd);
});
},
writeJsonxlToFile(filepath) {
return writeFileAction(filepath || defaultFilename('cpuprofile.jsonxl'), (destFilename) => {
fs.writeFileSync(destFilename, jsonxl.encode(data));
});
},
writeReport(filepath) {
getReport().writeToFile(filepath);
},
openReport() {
getReport().open();
},
get report() {
return getReport();
}
});
profiles.set(capture, result);
for (const callback of onEndCallbacks) {
callback(result);
}
return result;
}
module.exports = {
profile,
mark,
profileEnd,
writeToFile: (...args) => profile().writeToFile(...args),
writeJsonxlToFile: (...args) => profile().writeJsonxlToFile(...args),
writeReport: (...args) => profile().writeReport(...args),
openReport: (...args) => profile().openReport(...args)
};