-
Notifications
You must be signed in to change notification settings - Fork 306
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
base: master
Are you sure you want to change the base?
Conversation
Overall package sizeSelf size: 7.82 MB Dependency sizes| name | version | self size | total size | |------|---------|-----------|------------| | @datadog/native-appsec | 8.2.1 | 19.18 MB | 19.19 MB | | @datadog/native-iast-taint-tracking | 3.2.0 | 13.9 MB | 13.91 MB | | @datadog/pprof | 5.4.1 | 9.76 MB | 10.13 MB | | protobufjs | 7.2.5 | 2.77 MB | 5.16 MB | | @datadog/native-iast-rewriter | 2.5.0 | 2.51 MB | 2.65 MB | | @opentelemetry/core | 1.14.0 | 872.87 kB | 1.47 MB | | @datadog/native-metrics | 2.0.0 | 898.77 kB | 1.3 MB | | @opentelemetry/api | 1.8.0 | 1.21 MB | 1.21 MB | | import-in-the-middle | 1.11.2 | 112.74 kB | 826.22 kB | | msgpack-lite | 0.1.26 | 201.16 kB | 281.59 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | lru-cache | 7.18.3 | 133.92 kB | 133.92 kB | | pprof-format | 2.1.0 | 111.69 kB | 111.69 kB | | @datadog/sketches-js | 2.1.0 | 109.9 kB | 109.9 kB | | semver | 7.6.3 | 95.82 kB | 95.82 kB | | lodash.sortby | 4.7.0 | 75.76 kB | 75.76 kB | | ignore | 5.3.1 | 51.46 kB | 51.46 kB | | int64-buffer | 0.1.10 | 49.18 kB | 49.18 kB | | shell-quote | 1.8.1 | 44.96 kB | 44.96 kB | | istanbul-lib-coverage | 3.2.0 | 29.34 kB | 29.34 kB | | rfdc | 1.3.1 | 25.21 kB | 25.21 kB | | tlhunter-sorted-set | 0.1.0 | 24.94 kB | 24.94 kB | | limiter | 1.1.5 | 23.17 kB | 23.17 kB | | dc-polyfill | 0.1.4 | 23.1 kB | 23.1 kB | | retry | 0.13.1 | 18.85 kB | 18.85 kB | | jest-docblock | 29.7.0 | 8.99 kB | 12.76 kB | | crypto-randomuuid | 1.0.0 | 11.18 kB | 11.18 kB | | koalas | 1.0.2 | 6.47 kB | 6.47 kB | | path-to-regexp | 0.1.10 | 6.38 kB | 6.38 kB | | module-details-from-path | 1.0.3 | 4.47 kB | 4.47 kB |🤖 This report was automatically generated by heaviest-objects-in-the-universe |
BenchmarksBenchmark execution time: 2024-10-30 10:39:29 Comparing candidate commit bcd2b3e in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 258 metrics, 8 unstable metrics. |
const breakpointIdToProbe = new Map() | ||
|
||
function findScriptFromPartialPath (path) { | ||
return scriptIds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ this we could easily generalized for both DI and TV
const stack = 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 | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ this can be extracted
@@ -0,0 +1,106 @@ | |||
const { parentPort } = require('worker_threads') | |||
// TODO: move session to common place | |||
const session = require('../../../debugger/devtools_client/session') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
session should be simple to move
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on second thought, session
should probably go together with inspector_promises_polyfill.js
and state.js
since they seem to be self contained? I'm honestly OK with importing code directly here, as this code is evolving quickly. I don't know if it makes sense to invest on this now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thoughts @watson ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TL;DR: Leave session.js
where it is for now.
This file currently acts a singleton, so it can't both be called from your code and from DI in the same worker thread. But I don't assume that's ever going to happen - just an observation.
If we were to move this to a generic shared location, there's a chance that others might use it for other things in the future. If so, this singleton behavior might throw someone for a loop. But since it's such simple code, I don't see any good reason to make this into a shared module. The inspector_promises_polyfill.js
file on the other hand, should me moved to a central location.
So normally my conclusion to the above would be that we should each have our own version of session.js
that just called a shared inspector_promises_polyfill.js
. However, since the current session.js
so used all over the place in my code, it would mean that all that would have to be refactored to allow a session object to be passed into the called functions. And I think that's too big of a change right now - hence my comment at the top about leaving it as-is for now.
// TODO: move session to common place | ||
const session = require('../../../debugger/devtools_client/session') | ||
// TODO: move getLocalStateForCallFrame to common place | ||
const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might be a bit trickier but no reason not to do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Especially if we use two different session.js
files as I just laid out in my previous comment. If so, we need a way to pass in the current session
object. So for now, I'd just leave this as-is
e794cc6
to
c20ae05
Compare
@@ -32,8 +32,17 @@ module.exports = { | |||
.sort(([a], [b]) => a.length - b.length)[0] | |||
}, | |||
|
|||
getScriptUrlFromId (id) { |
There was a problem hiding this comment.
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
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #4826 +/- ##
==========================================
- Coverage 79.17% 72.87% -6.30%
==========================================
Files 273 66 -207
Lines 12427 3278 -9149
==========================================
- Hits 9839 2389 -7450
+ Misses 2588 889 -1699 ☔ View full report in Codecov by Sentry. |
// 2. Promise that's resolved when the breakpoint is set | ||
// 3. Promise that's resolved when the breakpoint is hit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to be able to sync on these two to be able to know when to start a test (a test can't start before the breakpoint is set) and when a dynamic instrumentation log can be created (if a breakpoint is hit). Maybe I could use callbacks for the same? Open to ideas here
} | ||
|
||
isReady () { | ||
return this._readyPromise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is just to make sure we don't attempt to set probes before the worker is online
@@ -0,0 +1,29 @@ | |||
'use strict' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ℹ️ script we run in the test to grab local variable data
this.worker.on('message', (message) => { | ||
const { type } = message | ||
if (type === BREAKPOINT_SET) { | ||
const { probeId } = message | ||
const resolve = probeIdToResolveBreakpointSet.get(probeId) | ||
if (resolve) { | ||
resolve() | ||
probeIdToResolveBreakpointSet.delete(probeId) | ||
} | ||
} else if (type === BREAKPOINT_HIT) { | ||
const { snapshot } = message | ||
const { probe: { id: probeId } } = snapshot | ||
const resolve = probeIdToResolveBreakpointHit.get(probeId) | ||
if (resolve) { | ||
resolve({ snapshot }) | ||
probeIdToResolveBreakpointHit.delete(probeId) | ||
} | ||
} | ||
}).unref() // We also need to unref this message handler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of using a type of BREAKPOINT_SET
and BREAKPOINT_HIT
that you send over the message channel, you could also just use two different distinct MessageChannel
instances. That way you always knew that if a message was received on the breakpointSetChannel
vs the breakpointHitChannel
what it meant. And you'd also avoid this if/else blocks.
You can see how I use those here:
dd-trace-js/packages/dd-trace/src/debugger/index.js
Lines 24 to 25 in 57f8a10
const rcChannel = new MessageChannel() | |
configChannel = new MessageChannel() |
this.worker.on('message', (message) => { | ||
const { type } = message | ||
if (type === BREAKPOINT_SET) { | ||
const { probeId } = message | ||
const resolve = probeIdToResolveBreakpointSet.get(probeId) | ||
if (resolve) { | ||
resolve() | ||
probeIdToResolveBreakpointSet.delete(probeId) | ||
} | ||
} else if (type === BREAKPOINT_HIT) { | ||
const { snapshot } = message | ||
const { probe: { id: probeId } } = snapshot | ||
const resolve = probeIdToResolveBreakpointHit.get(probeId) | ||
if (resolve) { | ||
resolve({ snapshot }) | ||
probeIdToResolveBreakpointHit.delete(probeId) | ||
} | ||
} | ||
}).unref() // We also need to unref this message handler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume no calling .unref()
here gave you problems?
@@ -0,0 +1,88 @@ | |||
const { parentPort } = require('worker_threads') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No 'use strict'
?
@@ -0,0 +1,106 @@ | |||
const { parentPort } = require('worker_threads') | |||
// TODO: move session to common place | |||
const session = require('../../../debugger/devtools_client/session') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TL;DR: Leave session.js
where it is for now.
This file currently acts a singleton, so it can't both be called from your code and from DI in the same worker thread. But I don't assume that's ever going to happen - just an observation.
If we were to move this to a generic shared location, there's a chance that others might use it for other things in the future. If so, this singleton behavior might throw someone for a loop. But since it's such simple code, I don't see any good reason to make this into a shared module. The inspector_promises_polyfill.js
file on the other hand, should me moved to a central location.
So normally my conclusion to the above would be that we should each have our own version of session.js
that just called a shared inspector_promises_polyfill.js
. However, since the current session.js
so used all over the place in my code, it would mean that all that would have to be refactored to allow a session object to be passed into the called functions. And I think that's too big of a change right now - hence my comment at the top about leaving it as-is for now.
// TODO: move session to common place | ||
const session = require('../../../debugger/devtools_client/session') | ||
// TODO: move getLocalStateForCallFrame to common place | ||
const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Especially if we use two different session.js
files as I just laid out in my previous comment. If so, we need a way to pass in the current session
object. So for now, I'd just leave this as-is
// Dynamic Instrumentation - Test Visibility not currently supported for windows | ||
if (process.platform === 'win32') { | ||
done() | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not move this whole block to the root of the spec file before the first call to describe
?
|
||
assert.deepEqual(captures, { | ||
lines: { | ||
9: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider if you can re-use the getBreakpointInfo
function for you tests. That way, if you ever change the line your breakpoint is on, your tests just adapt automatically.
if (!probe) { | ||
return session.post('Debugger.resume') | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this just an escape hatch in case the state is somehow messed up, is there a valid use-case for a breakpoint to exist, but no matching probe?
If this is just an escape hatch, I'd probably add a log line to log that you encountered this issue. But I'll leave that up you :)
What does this PR do?
This creates a
TestVisDynamicInstrumentation
class which includes a simplified version of the logic inpackages/dd-trace/src/debugger/index.js
:worker_thread
is started inTestVisDynamicInstrumentation#start
that'll wait for messages from the parent process.Motivation
Continuing the work for the integration between test visibility and dynamic instrumentation started in #4821
Plugin Checklist