Last active
April 13, 2025 21:43
-
-
Save JosePedroDias/69e287d3fed4b699d8d1ccd3ac8e2be5 to your computer and use it in GitHub Desktop.
ad hoc instrument a js file for coverage and sequencial call analysis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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