-
-
Notifications
You must be signed in to change notification settings - Fork 36k
inspector: add --cond to node inspect probe mode #64328
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
Open
joyeecheung
wants to merge
1
commit into
nodejs:main
Choose a base branch
from
joyeecheung:probe-cond
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ const { | |
| StringPrototypeIncludes, | ||
| StringPrototypeSlice, | ||
| StringPrototypeStartsWith, | ||
| StringPrototypeTrim, | ||
| Symbol, | ||
| } = primordials; | ||
|
|
||
|
|
@@ -87,6 +88,8 @@ const kInspectPortRegex = /^--inspect-port=(\d+)$/; | |
| * @typedef {object} Probe | ||
| * @property {string} expr Expression to evaluate on hit. | ||
| * @property {ProbeTarget} target User's original --probe request shape. | ||
| * @property {string} [condition] Condition from --cond. V8 only breaks when it is truthy. | ||
| * Scoped to the location, so probes sharing a location all carry the same value. | ||
| * @property {number} maxHit Per-probe hit limit from --max-hit. Infinity when unlimited. | ||
| * @property {number} hits Count of hits observed. | ||
| */ | ||
|
|
@@ -130,6 +133,12 @@ function formatTargetText(target) { | |
| return column === undefined ? `${suffix}:${line}` : `${suffix}:${line}:${column}`; | ||
| } | ||
|
|
||
| // Identity of a probe location. Probes sharing a key share one V8 breakpoint, | ||
| // so this must stay in sync between condition validation and breakpoint setup. | ||
| function locationKey(target) { | ||
| return `${target.suffix}\n${target.line}\n${target.column ?? ''}`; | ||
| } | ||
|
|
||
| function formatPendingProbeLocations(probes, pending) { | ||
| const seen = new SafeSet(); | ||
| for (const probeIndex of pending) { | ||
|
|
@@ -391,6 +400,22 @@ function parseProbeTokens(tokens, args) { | |
| probe.maxHit = parseUnsignedInteger(token.value, 'max-hit'); | ||
| break; | ||
| } | ||
| case 'cond': { | ||
| if (probes.length === 0) { | ||
| throw new ERR_DEBUGGER_STARTUP_ERROR('Unexpected --cond before --probe'); | ||
| } | ||
| // A blank condition does not act as a real predicate in V8 (an empty | ||
| // string always breaks), so reject it rather than silently mislead. | ||
| if (token.value === undefined || StringPrototypeTrim(token.value) === '') { | ||
| throw new ERR_DEBUGGER_STARTUP_ERROR(`Missing value for ${token.rawName}`); | ||
| } | ||
| const probe = probes[probes.length - 1]; | ||
| if (probe.condition !== undefined) { | ||
| throw new ERR_DEBUGGER_STARTUP_ERROR('A --probe can have at most one --cond'); | ||
| } | ||
| probe.condition = token.value; | ||
| break; | ||
| } | ||
| default: | ||
| if (probes.length > 0) { | ||
| throw new ERR_DEBUGGER_STARTUP_ERROR( | ||
|
|
@@ -410,6 +435,21 @@ function parseProbeTokens(tokens, args) { | |
| 'Probe mode requires at least one --probe <loc> --expr <expr> group'); | ||
| } | ||
|
|
||
| // V8 allows only one breakpoint per location, so probes sharing a location | ||
| // cannot carry different conditions. | ||
| const conditionByLocation = new SafeMap(); | ||
| for (const { target, condition } of probes) { | ||
| const key = locationKey(target); | ||
| if (conditionByLocation.has(key)) { | ||
| if (conditionByLocation.get(key) !== condition) { | ||
|
Comment on lines
+443
to
+444
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming this is structured into separate |
||
| throw new ERR_DEBUGGER_STARTUP_ERROR( | ||
| `Probes at ${formatTargetText(target)} must use the same --cond (or none)`); | ||
| } | ||
| } else { | ||
| conditionByLocation.set(key, condition); | ||
| } | ||
| } | ||
|
|
||
| const childArgv = ArrayPrototypeSlice(args, childStartIndex); | ||
| if (childArgv.length === 0) { | ||
| throw new ERR_DEBUGGER_STARTUP_ERROR('Probe mode requires a child script'); | ||
|
|
@@ -479,8 +519,8 @@ class ProbeInspectorSession { | |
| this.resolveCompletion = resolve; | ||
| /** @type {Probe[]} */ | ||
| this.probes = ArrayPrototypeMap(options.probes, | ||
| ({ expr, target, maxHit }) => | ||
| ({ expr, target, maxHit: maxHit ?? Infinity, hits: 0 })); | ||
| ({ expr, target, maxHit, condition }) => | ||
| ({ expr, target, condition, maxHit: maxHit ?? Infinity, hits: 0 })); | ||
| this.onChildOutput = FunctionPrototypeBind(this.onChildOutput, this); | ||
| this.onChildExit = FunctionPrototypeBind(this.onChildExit, this); | ||
| this.onClientClose = FunctionPrototypeBind(this.onClientClose, this); | ||
|
|
@@ -887,17 +927,19 @@ class ProbeInspectorSession { | |
| const uniqueTargets = new SafeMap(); | ||
|
|
||
| for (let probeIndex = 0; probeIndex < this.probes.length; probeIndex++) { | ||
| const { target } = this.probes[probeIndex]; | ||
| const key = `${target.suffix}\n${target.line}\n${target.column ?? ''}`; | ||
| const { target, condition } = this.probes[probeIndex]; | ||
| // Probes at the same location share one V8 breakpoint. parseProbeTokens has | ||
| // already ensured they carry the same condition. | ||
| const key = locationKey(target); | ||
| let entry = uniqueTargets.get(key); | ||
| if (entry === undefined) { | ||
| entry = { target, probeIndices: [] }; | ||
| entry = { target, condition, probeIndices: [] }; | ||
| uniqueTargets.set(key, entry); | ||
| } | ||
| ArrayPrototypePush(entry.probeIndices, probeIndex); | ||
| } | ||
|
|
||
| for (const { target, probeIndices } of uniqueTargets.values()) { | ||
| for (const { target, condition, probeIndices } of uniqueTargets.values()) { | ||
| // On Windows, normalize backslashes to forward slashes so the regex matches | ||
| // V8 script URLs which always use forward slashes. | ||
| const normalizedFile = process.platform === 'win32' ? | ||
|
|
@@ -918,6 +960,9 @@ class ProbeInspectorSession { | |
| // the inspector bind to the first executable column. | ||
| params.columnNumber = target.column - 1; | ||
| } | ||
| if (condition !== undefined) { | ||
| params.condition = condition; | ||
| } | ||
|
|
||
| const result = await this.callCdp('Debugger.setBreakpointByUrl', params); | ||
| debug('breakpoint set: id=%s urlRegex=%s locations=%j', | ||
|
|
@@ -943,9 +988,10 @@ class ProbeInspectorSession { | |
| code: exitCode, | ||
| report: { | ||
| v: kProbeVersion, | ||
| probes: ArrayPrototypeMap(this.probes, ({ expr, target, maxHit }) => { | ||
| // Omit an unlimited maxHit, as Infinity would serialize to null in JSON. | ||
| probes: ArrayPrototypeMap(this.probes, ({ expr, target, maxHit, condition }) => { | ||
| const probe = { expr, target }; | ||
| if (condition !== undefined) { probe.condition = condition; } | ||
| // Omit an unlimited maxHit, as Infinity would serialize to null in JSON. | ||
| if (maxHit !== Infinity) { probe.maxHit = maxHit; } | ||
| return probe; | ||
| }), | ||
|
|
||
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // This tests that probe mode rejects malformed --cond usage. | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| common.skipIfInspectorDisabled(); | ||
|
|
||
| const fixtures = require('../common/fixtures'); | ||
| const { assertProbeCliError } = require('../common/debugger-probe'); | ||
|
|
||
| const cwd = fixtures.path('debugger'); | ||
|
|
||
| assertProbeCliError( | ||
| ['--cond', 'x', '--probe', 'probe.js:12', '--expr', 'finalValue', 'probe.js'], | ||
| /Unexpected --cond before --probe/, { cwd }); | ||
|
|
||
| assertProbeCliError( | ||
| ['--probe', 'probe.js:12', '--cond', 'x', '--expr', 'finalValue', 'probe.js'], | ||
| /Each --probe must be followed immediately by --expr/, { cwd }); | ||
|
|
||
| assertProbeCliError( | ||
| ['--probe', 'probe.js:12', '--expr', 'finalValue', '--cond', 'x', '--cond', 'y', 'probe.js'], | ||
| /A --probe can have at most one --cond/, { cwd }); | ||
|
|
||
| assertProbeCliError( | ||
| ['--probe', 'probe.js:12', '--expr', 'finalValue', '--cond', ' ', 'probe.js'], | ||
| /Missing value for --cond/, { cwd }); | ||
|
|
||
| assertProbeCliError( | ||
| ['--probe', 'probe.js:12', '--expr', 'finalValue', '--cond'], | ||
| /Missing value for --cond/, { cwd }); | ||
|
|
||
| assertProbeCliError( | ||
| ['--probe', 'probe.js:12', '--expr', 'a', '--cond', 'x', | ||
| '--probe', 'probe.js:12', '--expr', 'b', '--cond', 'y', 'probe.js'], | ||
| /Probes at probe\.js:12 must use the same --cond \(or none\)/, { cwd }); | ||
|
|
||
| assertProbeCliError( | ||
| ['--probe', 'probe.js:12', '--expr', 'a', '--cond', 'x', | ||
| '--probe', 'probe.js:12', '--expr', 'b', 'probe.js'], | ||
| /Probes at probe\.js:12 must use the same --cond \(or none\)/, { cwd }); |
This file contains hidden or 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| // This tests that --cond and --max-hit work together. | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| common.skipIfInspectorDisabled(); | ||
|
|
||
| const fixtures = require('../common/fixtures'); | ||
| const { spawnSyncAndAssert } = require('../common/child_process'); | ||
| const { assertProbeJson } = require('../common/debugger-probe'); | ||
|
|
||
| const cwd = fixtures.path('debugger'); | ||
| const probeUrl = fixtures.fileURL('debugger', 'probe-max-hit.js').href; | ||
|
|
||
| // --max-hit written before --cond. The condition still filters index !== 1, so | ||
| // the only recorded hit carries value 1 rather than the loop's first iteration. | ||
| spawnSyncAndAssert(process.execPath, [ | ||
| 'inspect', | ||
| '--json', | ||
| '--probe', 'probe-max-hit.js:5', | ||
| '--expr', 'index', | ||
| '--max-hit', '5', | ||
| '--cond', 'index === 1', | ||
| 'probe-max-hit.js', | ||
| ], { cwd }, { | ||
| stdout(output) { | ||
| assertProbeJson(output, { | ||
| v: 2, | ||
| probes: [{ | ||
| expr: 'index', | ||
| condition: 'index === 1', | ||
| maxHit: 5, | ||
| target: { suffix: 'probe-max-hit.js', line: 5 }, | ||
| }], | ||
| results: [ | ||
| { | ||
| probe: 0, | ||
| event: 'hit', | ||
| hit: 1, | ||
| location: { url: probeUrl, line: 5, column: 3 }, | ||
| result: { type: 'number', value: 1, description: '1' }, | ||
| }, | ||
| { event: 'completed' }, | ||
| ], | ||
| }); | ||
| }, | ||
| trim: true, | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
An example that shows
--condbeing used would be good :-)