Skip to content

Instantly share code, notes, and snippets.

@JosePedroDias
Last active April 13, 2025 21:43
Show Gist options
  • Save JosePedroDias/69e287d3fed4b699d8d1ccd3ac8e2be5 to your computer and use it in GitHub Desktop.
Save JosePedroDias/69e287d3fed4b699d8d1ccd3ac8e2be5 to your computer and use it in GitHub Desktop.
ad hoc instrument a js file for coverage and sequencial call analysis
import { parse } from 'acorn';
import { simple } from 'acorn-walk';
import { generate } from 'escodegen';
import fs from 'fs';
// https://astexplorer.net/
function addCounterToNode(node) {
if (node.loc) {
const li = node.loc.start.line;
const assignment = {
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: '_mark_',
},
arguments: [
{
type: 'Literal',
value: li,
},
],
},
};
const traceArgs = {
"type": "TryStatement",
"block": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "warn"
}
},
"property": {
"type": "Identifier",
"name": "apply"
}
},
"arguments": [
{
"type": "Literal",
"value": null,
"raw": "null"
},
{
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "Array"
},
"property": {
"type": "Identifier",
"name": "from"
}
},
"arguments": [
{
"type": "Identifier",
"name": "arguments"
}
]
}
]
}
}
]
},
"handler": {
"type": "CatchClause",
"param": {
"type": "Identifier",
"name": "_"
},
"body": {
"type": "BlockStatement",
"body": []
}
},
"finalizer": null
};
const node2 = node.body.body;
node2.unshift(traceArgs); // comment this line if generating too much noise
node2.unshift(assignment);
}
}
function annotateWithCounters(jsCode) {
// https://github.com/acornjs/acorn/tree/master/acorn#interface
const ast = parse(jsCode, {
ecmaVersion: 'latest',
sourceType: 'module',
locations: true,
});
if (false) { // store AST for debugging
fs.writeFileSync('ast.json',
JSON.stringify(
ast,
(key, value) => typeof value === 'bigint' ? value.toString() : ['loc', 'start', 'end', 'raw'].includes(key) ? undefined : value,
2,
)
);
}
simple(ast, {
FunctionDeclaration(node) { addCounterToNode(node) },
FunctionExpression(node) { addCounterToNode(node) },
});
const instrumentedCode = generate(ast);
const lines = jsCode.toString().split('\n')
.map(l => l.replaceAll('${', '{').replaceAll('`', '\\`'));
const joinedLines = lines.join('`,\n `');
return `
// ** START OF INSTRUMENTATION HEADER **
const __lines__ = [
\`${joinedLines}\`
];
const __cov__ = {};
const __order__ = [];
const __orderSet__ = new Set();
console.warn('** instrumented ** eval copy(_analyze_()) to get measurements');
function _mark_(li) {
if (!__orderSet__.has(li)) {
__orderSet__.add(li);
__order__.push(li);
}
if (!__cov__[li]) {
__cov__[li] = 1;
console.warn('#' + li + ' ' + __lines__[li-1]);
}
else ++__cov__[li];
}
function _analyze_() {
const arr = Object.keys(__cov__).map((k) => [parseFloat(k), __cov__[k]]);
arr.sort((a, b) => b[1] - a[1]);
return { histogram: arr, order: __order__ };
}
window._analyze_ = _analyze_;
// ** END OF INSTRUMENTATION HEADER **
${instrumentedCode}
`;
}
function instrument(inFile) {
const outFile = inFile.replace(/\.js$/, '-instrumented.js');
console.log(`instrumenting ${inFile} -> ${outFile}`);
fs.readFile(inFile, (err, data) => {
if (err) return console.error('Error reading file:', err);
const instrumentedCode = annotateWithCounters(data);
fs.writeFile(outFile, instrumentedCode, (err) => {
if (err) console.error('Error writing instrumented file:', err);
});
});
}
const [inF] = process.argv.slice(2);
if (typeof inF !== 'string') {
console.error('Usage: node instrument.js <file.js>');
process.exit(1);
} else {
instrument(inF);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment