Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[test visibility] Simple dynamic instrumentation - test visibility client #4826

Merged
merged 13 commits into from
Nov 6, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use strict'

const { join } = require('path')
const { Worker } = require('worker_threads')
const { randomUUID } = require('crypto')
const log = require('../log')

const probeIdToResolvePromise = new Map()

class TestVisDynamicInstrumentation {
constructor () {
this.worker = null
}

// Return the snapshot id and a promise that's resolved if the breakpoint is hit
addLineProbe ({ file, line }) {
const snapshotId = randomUUID()

return [
snapshotId,
new Promise(resolve => {
const probeId = randomUUID()
probeIdToResolvePromise.set(probeId, resolve)
this.worker.postMessage({
snapshotId,
probe: { id: probeId, file, line }
})
})
]
}

start () {
if (this.worker) return

const { NODE_OPTIONS, ...envWithoutNodeOptions } = process.env

log.debug('Starting Test Visibility - Dynamic Instrumentation client...')

this.worker = new Worker(
join(__dirname, 'worker', 'index.js'),
{
execArgv: [],
env: envWithoutNodeOptions
}
)

// Allow the parent to exit even if the worker is still running
this.worker.unref()

this.worker.on('message', ({ snapshot }) => {
const { probe: { id: probeId } } = snapshot
const resolve = probeIdToResolvePromise.get(probeId)
if (resolve) {
resolve({ snapshot })
probeIdToResolvePromise.delete(probeId)
}
}).unref() // We also need to unref this message handler
}
}

module.exports = new TestVisDynamicInstrumentation()
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const { parentPort } = require('worker_threads')
juan-fernandez marked this conversation as resolved.
Show resolved Hide resolved
// TODO: move session to common place
const session = require('../../../debugger/devtools_client/session')
juan-fernandez marked this conversation as resolved.
Show resolved Hide resolved
// TODO: move getLocalStateForCallFrame to common place
const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot')
juan-fernandez marked this conversation as resolved.
Show resolved Hide resolved
// TODO: move findScriptFromPartialPath to common place
const {
findScriptFromPartialPath,
getStackFromCallFrames
} = require('../../../debugger/devtools_client/state')
const log = require('../../log')

let sessionStarted = false

const breakpointIdToSnapshotId = new Map()
const breakpointIdToProbe = new Map()

session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint], callFrames } }) => {
const probe = breakpointIdToProbe.get(hitBreakpoint)
if (!probe) {
return session.post('Debugger.resume')
}
juan-fernandez marked this conversation as resolved.
Show resolved Hide resolved

const stack = getStackFromCallFrames(callFrames)

const getLocalState = await getLocalStateForCallFrame(callFrames[0])

await session.post('Debugger.resume')

const snapshotId = breakpointIdToSnapshotId.get(hitBreakpoint)

const snapshot = {
id: snapshotId,
timestamp: Date.now(),
probe: {
id: probe.probeId,
version: '0',
location: probe.location
},
stack,
language: 'javascript'
}

const state = getLocalState()
if (state) {
snapshot.captures = {
lines: { [probe.location.lines[0]]: { locals: state } }
}
}

parentPort.postMessage({ snapshot })
})

// TODO: add option to remove breakpoint
parentPort.on('message', async ({ snapshotId, probe: { id: probeId, file, line } }) => {
await addBreakpoint(snapshotId, { probeId, file, line })
})

async function addBreakpoint (snapshotId, probe) {
if (!sessionStarted) await start()
const { file, line } = probe

probe.location = { file, lines: [String(line)] }

const script = findScriptFromPartialPath(file)
if (!script) throw new Error(`No loaded script found for ${file}`)

const [path, scriptId] = script

log.debug(`Adding breakpoint at ${path}:${line}`)

const { breakpointId } = await session.post('Debugger.setBreakpoint', {
location: {
scriptId,
lineNumber: line - 1
}
})

breakpointIdToProbe.set(breakpointId, probe)
breakpointIdToSnapshotId.set(breakpointId, snapshotId)
}

function start () {
sessionStarted = true
return session.post('Debugger.enable') // return instead of await to reduce number of promises created
}
13 changes: 2 additions & 11 deletions packages/dd-trace/src/debugger/devtools_client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { breakpoints } = require('./state')
const session = require('./session')
const { getLocalStateForCallFrame } = require('./snapshot')
const send = require('./send')
const { getScriptUrlFromId } = require('./state')
const { getStackFromCallFrames } = require('./state')
const { ackEmitting, ackError } = require('./status')
const { parentThreadId } = require('./config')
const log = require('../../log')
Expand Down Expand Up @@ -66,16 +66,7 @@ session.on('Debugger.paused', async ({ params }) => {
thread_name: threadName
}

const stack = params.callFrames.map((frame) => {
let fileName = getScriptUrlFromId(frame.location.scriptId)
if (fileName.startsWith('file://')) fileName = fileName.substr(7) // TODO: This might not be required
return {
fileName,
function: frame.functionName,
lineNumber: frame.location.lineNumber + 1, // Beware! lineNumber is zero-indexed
columnNumber: frame.location.columnNumber + 1 // Beware! columnNumber is zero-indexed
}
})
const stack = getStackFromCallFrames(params.callFrames)

// TODO: Send multiple probes in one HTTP request as an array (DEBUG-2848)
for (const probe of probes) {
Expand Down
13 changes: 11 additions & 2 deletions packages/dd-trace/src/debugger/devtools_client/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ module.exports = {
.sort(([a], [b]) => a.length - b.length)[0]
},

getScriptUrlFromId (id) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getScriptUrlFromId was only used for formatting the stack, which is now done by getStackFromCallFrames

return scriptUrls.get(id)
getStackFromCallFrames (callFrames) {
return callFrames.map((frame) => {
let fileName = scriptUrls.get(frame.location.scriptId)
if (fileName.startsWith('file://')) fileName = fileName.substr(7) // TODO: This might not be required
return {
fileName,
function: frame.functionName,
lineNumber: frame.location.lineNumber + 1, // Beware! lineNumber is zero-indexed
columnNumber: frame.location.columnNumber + 1 // Beware! columnNumber is zero-indexed
}
})
}
}

Expand Down
Loading