Created
March 17, 2021 02:27
-
-
Save bmeck/80ea055e6dc2ddedcc2aae884d8566c5 to your computer and use it in GitHub Desktop.
bradley needed to dump this devtools type tracer PoC somewhere
This file contains 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
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') }; | |
} |
This file contains 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
{ | |
"type": "module" | |
} |
This file contains 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 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