Skip to content

Commit

Permalink
[DI] Improve sampling tests (#4999)
Browse files Browse the repository at this point in the history
To test that multiple probes doesn't interfere with each others sample
rate, this commit also adds support for multiple breakpoints in a single
file.
  • Loading branch information
watson authored and rochdev committed Dec 18, 2024
1 parent 7101f3b commit 90775b3
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 19 deletions.
63 changes: 59 additions & 4 deletions integration-tests/debugger/basic.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('Dynamic Instrumentation', function () {
it('base case: target app should work as expected if no test probe has been added', async function () {
const response = await t.axios.get(t.breakpoint.url)
assert.strictEqual(response.status, 200)
assert.deepStrictEqual(response.data, { hello: 'foo' })
assert.deepStrictEqual(response.data, { hello: 'bar' })
})

describe('diagnostics messages', function () {
Expand Down Expand Up @@ -54,7 +54,7 @@ describe('Dynamic Instrumentation', function () {
t.axios.get(t.breakpoint.url)
.then((response) => {
assert.strictEqual(response.status, 200)
assert.deepStrictEqual(response.data, { hello: 'foo' })
assert.deepStrictEqual(response.data, { hello: 'bar' })
})
.catch(done)
} else {
Expand Down Expand Up @@ -245,7 +245,7 @@ describe('Dynamic Instrumentation', function () {
message: 'Hello World!',
logger: {
name: t.breakpoint.file,
method: 'handler',
method: 'fooHandler',
version,
thread_name: 'MainThread'
},
Expand Down Expand Up @@ -279,7 +279,7 @@ describe('Dynamic Instrumentation', function () {
const topFrame = payload['debugger.snapshot'].stack[0]
// path seems to be prefeixed with `/private` on Mac
assert.match(topFrame.fileName, new RegExp(`${t.appFile}$`))
assert.strictEqual(topFrame.function, 'handler')
assert.strictEqual(topFrame.function, 'fooHandler')
assert.strictEqual(topFrame.lineNumber, t.breakpoint.line)
assert.strictEqual(topFrame.columnNumber, 3)

Expand Down Expand Up @@ -375,6 +375,61 @@ describe('Dynamic Instrumentation', function () {

t.agent.addRemoteConfig(rcConfig)
})

it('should adhere to individual probes sample rate', function (done) {
const rcConfig1 = t.breakpoints[0].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } })
const rcConfig2 = t.breakpoints[1].generateRemoteConfig({ sampling: { snapshotsPerSecond: 1 } })
const state = {
[rcConfig1.config.id]: {
payloadsReceived: 0,
tiggerBreakpointContinuously () {
t.axios.get(t.breakpoints[0].url).catch(done)
this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10)
}
},
[rcConfig2.config.id]: {
payloadsReceived: 0,
tiggerBreakpointContinuously () {
t.axios.get(t.breakpoints[1].url).catch(done)
this.timer = setTimeout(this.tiggerBreakpointContinuously.bind(this), 10)
}
}
}

t.agent.on('debugger-diagnostics', ({ payload }) => {
const { probeId, status } = payload.debugger.diagnostics
if (status === 'INSTALLED') state[probeId].tiggerBreakpointContinuously()
})

t.agent.on('debugger-input', ({ payload }) => {
const _state = state[payload['debugger.snapshot'].probe.id]
_state.payloadsReceived++
if (_state.payloadsReceived === 1) {
_state.start = Date.now()
} else if (_state.payloadsReceived === 2) {
const duration = Date.now() - _state.start
clearTimeout(_state.timer)

// Allow for a variance of -5/+50ms (time will tell if this is enough)
assert.isAbove(duration, 995)
assert.isBelow(duration, 1050)

// Wait at least a full sampling period, to see if we get any more payloads
_state.timer = setTimeout(doneWhenCalledTwice, 1250)
} else {
clearTimeout(_state.timer)
done(new Error('Too many payloads received!'))
}
})

t.agent.addRemoteConfig(rcConfig1)
t.agent.addRemoteConfig(rcConfig2)

function doneWhenCalledTwice () {
if (doneWhenCalledTwice.calledOnce) return done()
doneWhenCalledTwice.calledOnce = true
}
})
})

describe('race conditions', function () {
Expand Down
8 changes: 6 additions & 2 deletions integration-tests/debugger/target-app/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ const Fastify = require('fastify')

const fastify = Fastify()

fastify.get('/:name', function handler (request) {
return { hello: request.params.name } // BREAKPOINT: /foo
fastify.get('/foo/:name', function fooHandler (request) {
return { hello: request.params.name } // BREAKPOINT: /foo/bar
})

fastify.get('/bar/:name', function barHandler (request) {
return { hello: request.params.name } // BREAKPOINT: /bar/baz
})

fastify.listen({ port: process.env.APP_PORT }, (err) => {
Expand Down
48 changes: 35 additions & 13 deletions integration-tests/debugger/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,43 @@ module.exports = {

function setup () {
let sandbox, cwd, appPort
const breakpoint = getBreakpointInfo(1) // `1` to disregard the `setup` function
const breakpoints = getBreakpointInfo(1) // `1` to disregard the `setup` function
const t = {
breakpoint,
breakpoint: breakpoints[0],
breakpoints,

axios: null,
appFile: null,
agent: null,

// Default to the first breakpoint in the file (normally there's only one)
rcConfig: null,
triggerBreakpoint,
generateRemoteConfig,
generateProbeConfig
triggerBreakpoint: triggerBreakpoint.bind(null, breakpoints[0].url),
generateRemoteConfig: generateRemoteConfig.bind(null, breakpoints[0]),
generateProbeConfig: generateProbeConfig.bind(null, breakpoints[0])
}

function triggerBreakpoint () {
// Allow specific access to each breakpoint
for (let i = 0; i < breakpoints.length; i++) {
t.breakpoints[i] = {
rcConfig: null,
triggerBreakpoint: triggerBreakpoint.bind(null, breakpoints[i].url),
generateRemoteConfig: generateRemoteConfig.bind(null, breakpoints[i]),
generateProbeConfig: generateProbeConfig.bind(null, breakpoints[i]),
...breakpoints[i]
}
}

function triggerBreakpoint (url) {
// Trigger the breakpoint once probe is successfully installed
t.agent.on('debugger-diagnostics', ({ payload }) => {
if (payload.debugger.diagnostics.status === 'INSTALLED') {
t.axios.get(breakpoint.url)
t.axios.get(url)
}
})
}

function generateRemoteConfig (overrides = {}) {
function generateRemoteConfig (breakpoint, overrides = {}) {
overrides.id = overrides.id || randomUUID()
return {
product: 'LIVE_DEBUGGING',
Expand All @@ -54,15 +69,19 @@ function setup () {
sandbox = await createSandbox(['fastify']) // TODO: Make this dynamic
cwd = sandbox.folder
// The sandbox uses the `integration-tests` folder as its root
t.appFile = join(cwd, 'debugger', breakpoint.file)
t.appFile = join(cwd, 'debugger', breakpoints[0].file)
})

after(async function () {
await sandbox.remove()
})

beforeEach(async function () {
t.rcConfig = generateRemoteConfig(breakpoint)
// Default to the first breakpoint in the file (normally there's only one)
t.rcConfig = generateRemoteConfig(breakpoints[0])
// Allow specific access to each breakpoint
t.breakpoints.forEach((breakpoint) => { breakpoint.rcConfig = generateRemoteConfig(breakpoint) })

appPort = await getPort()
t.agent = await new FakeAgent().start()
t.proc = await spawnProc(t.appFile, {
Expand Down Expand Up @@ -96,16 +115,19 @@ function getBreakpointInfo (stackIndex = 0) {
.slice(0, -1)
.split(':')[0]

// Then, find the corresponding file in which the breakpoint exists
// Then, find the corresponding file in which the breakpoint(s) exists
const file = join('target-app', basename(testFile).replace('.spec', ''))

// Finally, find the line number of the breakpoint
// Finally, find the line number(s) of the breakpoint(s)
const lines = readFileSync(join(__dirname, file), 'utf8').split('\n')
const result = []
for (let i = 0; i < lines.length; i++) {
const index = lines[i].indexOf(BREAKPOINT_TOKEN)
if (index !== -1) {
const url = lines[i].slice(index + BREAKPOINT_TOKEN.length + 1).trim()
return { file, line: i + 1, url }
result.push({ file, line: i + 1, url })
}
}

return result
}

0 comments on commit 90775b3

Please sign in to comment.