Skip to content

Instantly share code, notes, and snippets.

@bmeck
Created March 17, 2021 02:27
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 bmeck/80ea055e6dc2ddedcc2aae884d8566c5 to your computer and use it in GitHub Desktop.
Save bmeck/80ea055e6dc2ddedcc2aae884d8566c5 to your computer and use it in GitHub Desktop.
bradley needed to dump this devtools type tracer PoC somewhere
function foo(x, y) {
return Object;
}
const { stop } = trace();
new foo([1], new Date());
stop();
// boilerplate after this
import { fileURLToPath } from 'url';
import { Worker } from 'worker_threads';
import inspector from 'inspector';
function trace() {
inspector.open();
const worker = new Worker(
fileURLToPath(new URL('./tracer.js', import.meta.url))
);
worker.ref();
worker.on('message', (data) => {
if (data.type === 'done') {
worker.unref();
worker.terminate();
for (const [fnLocation, bindings] of Object.entries(data.data)) {
let returnValue = ['unknown'];
if (bindings['.returnValue']) {
returnValue = bindings['.returnValue'];
delete bindings['.returnValue'];
}
console.group(`typeof ${fnLocation} (`);
for (const [name, types] of Object.entries(bindings).sort((a, b) =>
a[0] < b[0] ? -1 : 1
)) {
console.log('%s: %s', name, types.join(' | '), ',');
}
console.groupEnd();
console.log(`) : ${returnValue.join(' | ')}`)
}
}
});
inspector.waitForDebugger();
return { stop: worker.postMessage.bind(worker, 'dump') };
}
{
"type": "module"
}
import inspector from 'inspector';
import { parentPort } from 'worker_threads';
const session = new inspector.Session();
session.connectToMainThread();
const send = (name, params) => {
// console.log('SENDING', name, params);
return new Promise((f, r) => {
const fn = (err, params) => {
if (err) return r(err);
f(params);
};
params ? session.post(name, params, fn) : session.post(name, fn);
});
};
const log = (...args) => {
return send('Runtime.callFunctionOn', {
functionDeclaration: `${function $() {
console.dir([...arguments], {depth: null});
}}`,
objectId: GetId,
arguments: args.map((a) => ({ value: a })),
disableBreaks: true,
returnByValue: true,
});
};
let pending = null;
function queueParallelWork(asyncFn) {
let prom = Promise.resolve(pending).finally(async () => {
await asyncFn();
if (pending === prom) pending = null;
});
return prom;
}
let scriptIdToScript = new Map();
let breakpoints = new Map();
let functions = new Map();
function locationKey(location) {
return `${location.lineNumber}:${location.columnNumber}:${location.scriptId}`;
}
/**
*
* @param {object} o
* @returns {Array<any>}
*/
function flatOwnDescriptors(o) {
let entries = [];
entries.push(Object.getPrototypeOf(o)?.constructor);
for (const k of [...Object.getOwnPropertyNames(o), ...Object.getOwnPropertySymbols(o)]) {
let desc = Object.getOwnPropertyDescriptor(o, k);
entries.push(k);
for (const [attr, value] of Object.entries(desc)) {
entries.push(attr, value);
}
};
return Object.setPrototypeOf(entries, null);
}
/**
*
* @param {Array<any>} arr
* @returns {object}
*/
function unflatOwnDescriptors(arr) {
let object = [];
let constructor = arr.shift();
for (let i = 0; i < arr.length; i++) {
let desc = Object.create(null);
let prop = arr.shift();
if (prop.name === 'length') {
continue;
}
let key = prop.value;
desc.name = key;
for (let ii = 0; ii < 4; ii++) {
let attr = arr.shift().value.value;
let value = arr.shift().value;
desc[attr] = value;
}
if (key.type === 'string') key = key.value;
else continue;
object.push(desc);
}
return {
constructor,
object
};
}
async function typeSignature(value) {
async function getProps() {
const descs = (
await send('Runtime.callFunctionOn', {
functionDeclaration: `function $() {
return (${flatOwnDescriptors})(this)
}`,
objectId: value.objectId,
})
).result;
let entries = (
await send('Runtime.getProperties', {
objectId: descs.objectId,
ownProperties: true,
})
).result;
// await log(entries);
const {constructor, object} = unflatOwnDescriptors(entries);
// await log(constructor, value, entries);
object.sort((a, b) => (a.name < b.name ? -1 : 1));
return {constructor, object};
}
if (value.type === 'undefined') {
return 'undefined';
} else if (value.type === 'number') {
return 'number';
} else if (value.type === 'string') {
return 'string';
} else if (value.type === 'boolean') {
return 'boolean';
} else if (value.type === 'symbol') {
return 'symbol';
} else if (value.type === 'bigint') {
return 'bigint';
} else if (value.type === 'object') {
if (value.subtype === 'null') {
return 'null';
}
let {constructor, object: props} = await getProps();
if (value.subtype === 'array') {
return `[${(
await Promise.all(
props
.filter((_) => _.name.type !== 'string' || _.name.value !== 'length')
.map(async (_) => `${await typeSignature(_.value)}`)
)
).join(', ')}]`;
} else {
if (constructor.value.type === 'function') {
const ret = await send('Runtime.getProperties', constructor.value);
const loc = ret.internalProperties.find(_ => _.name === '[[FunctionLocation]]');
if (loc) {
const {scriptId, lineNumber, columnNumber} = loc.value.value;
return `${
scriptIdToScript.get(scriptId).url
}:${lineNumber+1}:${columnNumber+1}`;
}
const { className } = value;
if (!['Object', 'Array'].includes(className)) {
return className;
}
}
return `{${(
await Promise.all(
props.map(
async (_) =>
`${JSON.stringify(_.name)}: ${await typeSignature(_.value)}`
)
)
).join(', ')}}`;
}
} else if (value.type === 'function') {
const ret = await send('Runtime.getProperties', value);
const loc = ret.internalProperties.find(_ => _.name === '[[FunctionLocation]]');
if (loc) {
const {scriptId, lineNumber, columnNumber} = loc.value.value;
return `typeof ${
scriptIdToScript.get(scriptId).url
}:${lineNumber+1}:${columnNumber+1}`;
}
return `typeof ${ret.result.find(_ => _.name === 'name').value.value}`;
}
// await log('unknown type', value);
}
session.on('Debugger.scriptParsed', ({ params: script }) => {
scriptIdToScript.set(script.scriptId, script);
if (script.url.startsWith('node:')) return;
queueParallelWork(async () => {
const { locations } = await send('Debugger.getPossibleBreakpoints', {
start: {
scriptId: script.scriptId,
lineNumber: 0,
},
});
breakpoints.set(script.scriptId, locations);
for (let location of locations) {
await send('Debugger.setBreakpoint', { location });
}
});
});
session.on('Debugger.paused', async (args) => {
// await log(args);
let topFrame = args.params.callFrames[0];
// console.dir(args, { depth: null });
trace: if (breakpoints.has(topFrame.location.scriptId)) {
const fnKey = locationKey(topFrame.functionLocation);
const locKey = locationKey(topFrame.location);
let fnData = functions.get(fnKey);
if (!fnData) {
const locations = breakpoints.get(topFrame.location.scriptId);
let left = -1;
let right = -1;
for (let i = 0; i < locations.length; i++) {
let location = locations[i];
if (topFrame.functionLocation) {
const { functionLocation } = topFrame;
if (
(left === -1 &&
location.lineNumber > functionLocation.lineNumber) ||
location.lineNumber === functionLocation.lineNumber ||
location.columnNumber >= functionLocation.columnNumber
) {
left = i;
}
}
if (
location.lineNumber > topFrame.location.lineNumber ||
(location.lineNumber === topFrame.location.lineNumber &&
location.columnNumber > topFrame.location.columnNumber)
) {
right = i;
break;
}
}
if (right - left !== 1) break trace;
functions.set(
fnKey,
(fnData = {
types: new Map(),
firstBreak: locationKey(locations[left]),
})
);
} else if (fnData.firstBreak !== locKey) {
// break trace;
}
let types = fnData.types;
const add = async (name, value) => {
if (!types.has(name)) {
types.set(name, new Set());
}
types.get(name).add(await typeSignature(value));
};
if (topFrame.returnValue) {
add('.returnValue', topFrame.returnValue);
}
let scope = await send('Runtime.getProperties', {
objectId: topFrame.scopeChain[0].object.objectId,
});
let bindings = [...scope.result, { name: 'this', value: topFrame.this }];
for (let binding of bindings) {
if (binding.value.objectId) {
// await send('Runtime.releaseObject', {
// objectId: binding.value.objectId,
// });
}
let result = (
await send('Debugger.evaluateOnCallFrame', {
callFrameId: topFrame.callFrameId,
expression: binding.name,
objectGroup: 'tracer',
})
).result;
if (result.objectId) {
// await log({ result }, { depth: null });
let p = {
functionDeclaration: `${function $(getId, value) {
return getId(value);
}}`,
objectId: GetId,
arguments: [{ objectId: GetId }, result],
returnByValue: true,
// objectGroup: 'tracer',
};
let {
result: { value: id },
} = await send('Runtime.callFunctionOn', p);
// await log('added', binding.name, id);
await add(binding.name, result);
} else {
await add(binding.name, result);
}
}
}
while (pending !== null) {
await pending;
}
session.post('Debugger.resume');
});
parentPort.on('message', async (msg) => {
if (msg === 'dump') {
let acc = Object.create(null);
for (let [loc, data] of functions) {
let [, line, col, scriptId] = /([^:]*):([^:]*):([\s\S]*)/.exec(loc);
let script = scriptIdToScript.get(scriptId);
let url = script.url;
acc[`${url}:${+line + 1}:${+col + 1}`] = Object.fromEntries(
[...data.types.entries()].map(([k, v]) => [k, Array.from(v)])
);
}
parentPort?.postMessage({ type: 'done', data: acc });
await send('Debugger.disable');
await send('Runtime.disable');
// session.disconnect();
process.exit();
}
});
await send('Runtime.enable');
await send('Debugger.enable');
await send('Debugger.setInstrumentationBreakpoint', {
instrumentation: 'beforeScriptExecution',
});
const {
result: { objectId: GetId },
} = await send('Runtime.evaluate', {
expression: `(${() => {
let ids = new WeakMap();
let has = ids.has.bind(ids);
let get = ids.get.bind(ids);
let set = ids.set.bind(ids);
let nextId = 1;
return (o) => {
if (!has(o)) {
let id = nextId;
nextId++;
set(o, id);
}
return get(o);
};
}})()`,
objectGroup: 'getId',
disableBreaks: true,
});
await send('Runtime.runIfWaitingForDebugger');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment