-
Notifications
You must be signed in to change notification settings - Fork 119
Conversation
Any particular reason for why we don't use https://github.com/Microsoft/vscode-chrome-debug/issues/445 |
Because it has that issue which might never be fixed, and this is blocking chrome-debug in VS. |
@roblourens @rakatyal It's blocking a very particular use case of HTML imports in Chrome, which is a platform issue, so we implement a workaround in our debugger? It's my understanding that break-on-load using the recommended way works for the vast majority of Chrome users? What's the pro's and con's of our own implementation of this? |
@auchenberg: I believe apart from the flag not working in the HTML imports scenario, there are some other reasons to go this way:
So we agree that this might not be the best solution out there, but it's a safe bet for us now. |
I understand that we have prior art from VS, but it's also my understanding that we were using the Chrome recommended API before we encountered the HTML imports issues, correct? We should therefore know the performance characteristics and behavior of the APIs. I don't understand why we don't want to offload this functionality to the platform (Chrome) by using an API that's exposed for this purpose and already well-used inside Chrome DevTools. As I see this then this approach is growing our implementation and maintainability surface within our debugger for no apparent gain. That said this is an engineering decision, and if you think this is the best solution now and going forward then that's the way we roll. |
@auchenberg: I agree that ideally we should be offloading this functionality to the platform. But from my understanding we never actually got to using that flag and hence never actually measured the performance impact of it. @digeff should be able to answer this concretely though, but we may think that flag would have had adverse performance impacts when compared to this approach. |
@auchenberg We never tested the performance of that flag. We did test the performance of using a breakpoint on .*:0:0 to break on the first line of each and every file, which is similar to that command. When using .*:0:0 in a project with 100s JavaScript file, the page took an extra 10 seconds to load the page, and as far as I could tell, 5 of those seconds where Chrome stopping and resuming the JS execution 100 times, so there was nothing we could do about that. The remaining open questions are:
I don't have those answers... |
Given that this core library is shared between Node, Chrome and other runtimes we should be careful about enabling this feature by default, if there's any performance overhead with larger amount of files. Ex: smaller Node projects has typically 500+ files. @digeff Does the current implementation add a significant overhead? I'm still a firm believer that we should offload as much as possible to the platform, and if there's a platform issue, like the issue with DOMDebugger.setInstrumentationBreakpoint, it should be fixed by a given platform team, and we shouldn't do workarounds in our debugger. |
src/chrome/chromeDebugAdapter.ts
Outdated
|
||
const mappedUrl = this._pathTransformer.scriptParsed(pausedScriptUrl); | ||
let normalizedMappedUrl: string; | ||
if (mappedUrl !== undefined) { |
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.
Could you please describe in which scenario will **mappedUrl ** be undefined, and what the code will do in that scenario?
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.
It shouldn't be. The base function just returns the value passed to it. I added it just to be sure because path.normalize throws an error when script path is undefined.
src/chrome/chromeDebugAdapter.ts
Outdated
// The file has no pending breakpoints to resolve. Just continue in this case. | ||
this.chrome.Debugger.resume() | ||
.catch(e => { | ||
logger.log("Failed to resume due to exception: " + e.message); |
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.
Should we also show a message to the user here?
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.
Hmm I am unsure about that one. @roblourens What do you think? I guess if this happens, Chrome will be stuck in paused state.
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.
You could change the logger.log to logger.error, then it will be sent to the debug console by default.
src/chrome/chromeDebugAdapter.ts
Outdated
|
||
// If the file has unbound breakpoints, make sure to resolve them first and then decide to continue or not | ||
if (this._pendingBreakpointsByUrl.has(normalizedMappedUrl)) { | ||
this.resolvePendingBreakpoint(this._pendingBreakpointsByUrl.get(normalizedMappedUrl)) |
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.
Normalizing paths is tricky. Could you add a logger message that says: We normalized AbC to abc, and we did (or didn't) find any breakpoints for that url? I think that could prove very useful in case we run into any issues
src/chrome/chromeDebugAdapter.ts
Outdated
} | ||
|
||
// If the file has unbound breakpoints, make sure to resolve them first and then decide to continue or not | ||
if (this._pendingBreakpointsByUrl.has(normalizedMappedUrl)) { |
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.
You could also just call this._pendingBreakpointsByUrl.get(normalizedMappedUrl) here, and test for undefined...
src/chrome/chromeDebugAdapter.ts
Outdated
|
||
// If the file has unbound breakpoints, make sure to resolve them first and then decide to continue or not | ||
if (this._pendingBreakpointsByUrl.has(normalizedMappedUrl)) { | ||
this.resolvePendingBreakpoint(this._pendingBreakpointsByUrl.get(normalizedMappedUrl)) |
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'd change this method to return some information about the breakpoint, so you can deduce _userBreakpointOnLine1Col1 from there, rather than having to store it on an instance variable...
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 don't wanna change the function signature just for my use case since I think it makes more sense the way it is right now. Do you see any major issue with the use of an instance variable?
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 don't understand the flag, don't you need to keep this info per-file?
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.
Yes I need this per file that's why I reset it every time if it becomes true. Since I am waiting for the resolvePendingBP request to complete before resetting it, I figured this should be okay and we won't need to store it per file since the operation will be synchronous for different files. Thoughts?
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.
So it relies on the scriptParsed and onPause happening right next to each other, and scriptParsed coming first? That's probably a safe assumption.
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.
Would help to document these assumptions.
src/chrome/chromeDebugAdapter.ts
Outdated
this._pendingBreakpointsByUrl.delete(normalizedMappedUrl); | ||
// If none of the breakpoints resolved in the file were at position (1,1) we should continue | ||
if (!this._userBreakpointOnLine1Col1) { | ||
this.chrome.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.
I would merge this line with line 530...
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -859,6 +935,10 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
return this.setBreakpoints(pendingBP.args, pendingBP.requestSeq, pendingBP.ids).then(response => { | |||
response.breakpoints.forEach((bp, i) => { | |||
bp.id = pendingBP.ids[i]; | |||
// If any of the unbound breakpoints in this file is on (1,1), we set userBreakpointOnLine1Col1 to true | |||
if (bp.line === 1 && bp.column === 1) { |
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.
@roblourens Given that Chrome tends to ignore column numbers, is this the best check to do here?
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.
What do you mean it ignores column numbers?
src/chrome/chromeDebugAdapter.ts
Outdated
|
||
// If all the breakpoints on this point are stopOnEntry breakpoints | ||
// This will be true in cases where it's a single breakpoint and it's a stopOnEntry breakpoint | ||
// This can also be true when we have multiple breakpoints and all of them are stopOnEntry breakpoints, for example in cases like index.js and index.bin.js |
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 don't understand the index.js/index.bin.js example
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.
Suppose user puts breakpoints in both index.js and index.bin.js files, when the setBreakpoints function is called for index.js it will set a stopOnEntry breakpoint on index. files which will also match index.bin.js.
Now when setBreakpoints is called for index.bin.js it will again put a stopOnEntry breakpoint in itself. So when the file is actually loaded, we would have 2 stopOnEntry breakpoints.
src/chrome/chromeDebugAdapter.ts
Outdated
const mappedUrl = this._pathTransformer.scriptParsed(pausedScriptUrl); | ||
let normalizedMappedUrl: string; | ||
if (mappedUrl !== undefined) { | ||
normalizedMappedUrl = path.normalize(mappedUrl); |
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 do you need path.normalize? I don't use that anywhere else.
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.
Mapped URL sometimes returned paths with // like E://folder//file while I stored the mappings for stopOnEntry dictionaries without it. So I changed to use normalized paths for consistencies.
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 does it do that? Do you know where the //
is coming from?
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -859,6 +935,10 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
return this.setBreakpoints(pendingBP.args, pendingBP.requestSeq, pendingBP.ids).then(response => { | |||
response.breakpoints.forEach((bp, i) => { | |||
bp.id = pendingBP.ids[i]; | |||
// If any of the unbound breakpoints in this file is on (1,1), we set userBreakpointOnLine1Col1 to true | |||
if (bp.line === 1 && bp.column === 1) { |
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.
What do you mean it ignores column numbers?
src/chrome/chromeDebugAdapter.ts
Outdated
|
||
// If the file has unbound breakpoints, make sure to resolve them first and then decide to continue or not | ||
if (this._pendingBreakpointsByUrl.has(normalizedMappedUrl)) { | ||
this.resolvePendingBreakpoint(this._pendingBreakpointsByUrl.get(normalizedMappedUrl)) |
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 don't understand the flag, don't you need to keep this info per-file?
src/chrome/chromeDebugAdapter.ts
Outdated
// The file has no pending breakpoints to resolve. Just continue in this case. | ||
this.chrome.Debugger.resume() | ||
.catch(e => { | ||
logger.log("Failed to resume due to exception: " + e.message); |
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.
You could change the logger.log to logger.error, then it will be sent to the debug console by default.
src/chrome/chromeDebugAdapter.ts
Outdated
.then(() => this._pendingBreakpointsByUrl.delete(source)); | ||
let normalizedSource: string; | ||
if (source !== undefined) { | ||
normalizedSource = path.normalize(source); |
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.
Don't use path.normalize without a reason, e.g. these can be URLs or remote paths.
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.
Hmm as mentioned above I don't see any other way to use it for consistency for file paths. Would you know of any other solution? Or else I will try to find a way to ignore this for URLs and remote paths somehow.
src/chrome/chromeDebugAdapter.ts
Outdated
return this.unverifiedBpResponseForBreakpoints(args, requestSeq, body.breakpoints, localize('bp.fail.unbound', "Breakpoints set but not yet bound"), true); | ||
// If it's a break on load script, we need to send the original args to avoid adjusting the line and column numbers twice | ||
if (this._stopOnEntryRequestedFileNameToBreakpointId.has(targetScriptUrl)) { | ||
return this.unverifiedBpResponseForBreakpoints(originalArgs, requestSeq, body.breakpoints, localize('bp.fail.unbound', "Breakpoints set but not yet bound"), true); |
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 should happen in either case, not just break on load, right?
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 think the current behavior is wrong so go ahead and change it.
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -1079,6 +1175,12 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
if (!args.source.path || args.source.sourceReference) return Promise.resolve(); | |||
|
|||
return this._sourceMapTransformer.getGeneratedPathFromAuthoredPath(args.source.path).then<void>(mappedPath => { | |||
|
|||
// Currently keeping it on under a flag | |||
if (this._breakOnLoadActive) { |
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.
Jump out before the async call, and more descriptive comment
src/chrome/chromeDebugAdapter.ts
Outdated
}); | ||
|
||
// If script has been parsed, script object won't be undefined and we would have the mapping file on the disk and we can directly set breakpoint using that | ||
if (script !== undefined) { |
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.
if (script) {
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.
At this point, the existing debugger can also set breakpoints in scripts before they are loaded, for node debugging where we know all the sourcemaps and paths. I think the check should only be for if (!_breakOnLoadActive || script) {
src/chrome/chromeDebugAdapter.ts
Outdated
const fileNameWithoutExtension = path.parse(fileNameWithoutFullPath).name; | ||
const escapedFileName = fileNameWithoutExtension.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||
|
||
return ".*[\\\\\\/]" + escapedFileName + "([^A-z].*)?$"; |
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.
Can you add a comment? I don't quite follow what this is matching
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -472,6 +481,60 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
} else if (notification.hitBreakpoints && notification.hitBreakpoints.length) { | |||
reason = 'breakpoint'; | |||
|
|||
const hitBreakpoints = notification.hitBreakpoints; |
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.
Move all of this to a helper method, and skip it if the flag is not set
@@ -1004,6 +1094,8 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
|
|||
return this.validateBreakpointsPath(args) | |||
.then(() => { | |||
// Deep copy args to originalArgs | |||
const originalArgs: ISetBreakpointsArgs = JSON.parse(JSON.stringify(args)); |
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.
Isn't there a better way to do this?
This feature has a lot of code. What do you guys @roblourens @rakatyal think of putting all the code relevant to this feature in some breakOnLoad.ts file/class and call methods on that file from every other place? That way it'll be easy to find all the relevant code, etc... |
src/chrome/chromeDebugAdapter.ts
Outdated
/* Constructs the regex for files to enable break on load | ||
For example, for a file index.js the regex will match urls containing index.js, index.ts, abc/index.ts, index.bin.js etc | ||
It won't match index100.js, indexabc.ts etc */ | ||
private getUrlRegexForBreakOnLoad(url: string): string { |
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.
Put this in chromeUtils.ts and write some unit tests for it
src/chrome/chromeDebugAdapter.ts
Outdated
} catch (e) { | ||
logger.log(`Exception occured while trying to set stop on entry breakpoint ${e.message}.`); | ||
} | ||
breakpointId = result.breakpointId; |
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.
If the above throws, then result
is null
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -131,6 +131,18 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
|
|||
private _lastPauseState: { expecting: ReasonType; event: Crdp.Debugger.PausedEvent }; | |||
|
|||
private _userBreakpointOnLine1Col1: boolean = false; | |||
|
|||
private _breakOnLoadActive: boolean = true; |
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.
Should not be true by default, you can set it to true if the launch config param is set
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -237,6 +249,10 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
this._sourceMapTransformer.launch(args); | |||
this._pathTransformer.launch(args); | |||
|
|||
if (args.useBreakOnLoadRegex === true) { | |||
this._useBreakOnLoadRegex = true; |
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.
Maybe the option should be "breakOnLoadStrategy" with "regex", "instrument", or "none" (default).
src/chrome/chromeDebugAdapter.ts
Outdated
* Checks and resolves the pending breakpoints of a script. If any breakpoints were resolved returns true, else false. | ||
* Used when break on load active, either through Chrome's Instrumentation Breakpoint API or the regex approach | ||
*/ | ||
private async ifResolvedPendingBreakpointsOfPausedScript(scriptId: string): Promise<boolean> { |
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.
Just call it resolvePendingBreakpointsOfPausedScript
How can we break up this code a little more? It's hard to break logic out of ChromeDebugAdapter, but there's so much just related to this one feature, that it might make sense to attempt that here. We could have a |
@roblourens: It would be tricky since we are deciding whether to pause/continue inside the onPaused and it might lead to some code duplication since we will have to store that state too and add extra code to handle the communication with the helper class we create. But if you feel strongly about this, I can try to do that. |
I think we should try. If you want to look over it together and decide what should go where, we can have a call or sit down together. But I see this adding a lot of code to core pathways which just deals with one feature, so I think it will be more readable if we can split it up a little. |
Okay. I will give it a try and reach out if I need directions. |
…API and Regex matching
@roblourens: Done some refactoring. Please have a look. |
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 overall really great, and is exactly what I had in mind. The fact that BreakOnLoadHelper is 200 lines long really justifies having a helper class for this feature.
I left a couple comments about moving even more into that class. I think it would be great if we can get rid of all places where chromeDebugAdapter checks the strategy type and does something different. It shouldn't even know that there are two strategies. It's ok to check it for 'none', that will probably simplify a couple things.
src/chrome/breakOnLoadHelper.ts
Outdated
|
||
// Sets a breakpoint on (0,0) for the files matching the given regex | ||
private async setStopOnEntryBreakpoint(urlRegex: string): Promise<Crdp.Debugger.SetBreakpointByUrlResponse> { | ||
let result = await this._chromeDebugAdapter.chrome.Debugger.setBreakpointByUrl({ urlRegex, lineNumber: 0, columnNumber: 0, condition: '' }); |
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.
You can just leave 'condition' out
src/chrome/chromeDebugAdapter.ts
Outdated
} else { // Else if script hasn't been parsed and break on load is active, we need to do extra processing | ||
|
||
// If the strategy is set to regex, we try to match the file where user put the breakpoint through a regex and tell Chrome to put a stop on entry breakpoint there | ||
if (this._breakOnLoadStrategy === 'regex') { |
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.
Can you move these few lines into the helper? Then it can check the strategy and do the right thing.
src/chrome/chromeDebugAdapter.ts
Outdated
// set break on load breakpoints. For those files, it is called from onPaused function. | ||
// For the default Chrome's API approach, we don't need to call resolvePendingBPs from inside scriptParsed | ||
if (this._breakOnLoadStrategy !== 'none') { | ||
if (this._breakOnLoadStrategy === 'regex' && !this._breakOnLoadHelper.stopOnEntryRequestedFileNameToBreakpointId.has(mappedUrl)) { |
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 a complicated check, can there be a method like shouldResovePendingBPs
on the helper?
src/chrome/chromeDebugAdapter.ts
Outdated
@@ -465,6 +486,19 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter { | |||
} else if (notification.hitBreakpoints && notification.hitBreakpoints.length) { | |||
reason = 'breakpoint'; | |||
|
|||
// If breakOnLoadStrategy is set to regex, we may have hit a stopOnEntry breakpoint we put. | |||
// So we need to resolve all the pending breakpoints in this script and then decide to continue or not | |||
if (this._breakOnLoadStrategy === 'regex') { |
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.
We could also hide details of the implementation inside the helper here. I'm thinking, before checking details of the notification, call a method onPaused
on the helper. It returns true if the helper handled it, and we should continue. If it returns false, we handle it in chromeDebugAdapter as usual.
src/chrome/breakOnLoadHelper.ts
Outdated
/* Constructs the regex for files to enable break on load | ||
For example, for a file index.js the regex will match urls containing index.js, index.ts, abc/index.ts, index.bin.js etc | ||
It won't match index100.js, indexabc.ts etc */ | ||
private getUrlRegexForBreakOnLoad(url: string): string { |
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.
Move to chromeUtils and write unit tests
Testing - the easiest way to test this will probably be tests in chrome-debug that launch chrome and use a real page. We could also add unit tests in vscode-chrome-debug-core, and breaking out this helper class could make that easier. Do you think we would benefit from unit tests here? |
Thanks for the great feedback Rob! Addressed most of it. I agree that the best way to test this will be in vscode-chrome-debug and I will start working on it. I am sure we can add some value by adding unit tests but I guess we will be in a better position to assess that once we have the tests written in chrome-debug first. We can then assess the areas that those tests aren't properly covering and then maybe write some unit tests for those. Thoughts? |
That sounds good to me |
This looks great, I'll merge this once there are tests in vscode-chrome-debug |
Woohu 🎉 |
No description provided.