From b1b5e201bfa8c19d992e3e18f17ae421092eee62 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 11 Dec 2024 18:26:12 +0100 Subject: [PATCH] [DI] Improve sampling tests 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. --- integration-tests/debugger/basic.spec.js | 63 +++++++++++++++++-- .../debugger/target-app/basic.js | 8 ++- integration-tests/debugger/utils.js | 48 ++++++++++---- 3 files changed, 100 insertions(+), 19 deletions(-) diff --git a/integration-tests/debugger/basic.spec.js b/integration-tests/debugger/basic.spec.js index 22a8ec98ff1..57c0c4a67a8 100644 --- a/integration-tests/debugger/basic.spec.js +++ b/integration-tests/debugger/basic.spec.js @@ -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 () { @@ -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 { @@ -245,7 +245,7 @@ describe('Dynamic Instrumentation', function () { message: 'Hello World!', logger: { name: t.breakpoint.file, - method: 'handler', + method: 'fooHandler', version, thread_name: 'MainThread' }, @@ -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) @@ -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 () { diff --git a/integration-tests/debugger/target-app/basic.js b/integration-tests/debugger/target-app/basic.js index 2fa9c16d221..d9d1e0e9185 100644 --- a/integration-tests/debugger/target-app/basic.js +++ b/integration-tests/debugger/target-app/basic.js @@ -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) => { diff --git a/integration-tests/debugger/utils.js b/integration-tests/debugger/utils.js index 1ea6cb9b54c..bca970dea87 100644 --- a/integration-tests/debugger/utils.js +++ b/integration-tests/debugger/utils.js @@ -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', @@ -54,7 +69,7 @@ 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 () { @@ -62,7 +77,11 @@ function setup () { }) 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, { @@ -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 }