Skip to content
This repository has been archived by the owner on Oct 2, 2021. It is now read-only.

Implementing break on load #241

Merged
merged 6 commits into from
Nov 28, 2017
Merged

Conversation

rakatyal
Copy link
Contributor

No description provided.

@auchenberg
Copy link
Contributor

Any particular reason for why we don't use DOMDebugger.setInstrumentationBreakpoint as per recommendation from the Chrome team?

https://github.com/Microsoft/vscode-chrome-debug/issues/445

@roblourens
Copy link
Member

Because it has that issue which might never be fixed, and this is blocking chrome-debug in VS.

@auchenberg
Copy link
Contributor

auchenberg commented Sep 27, 2017

@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?

@rakatyal
Copy link
Contributor Author

@auchenberg: I believe apart from the flag not working in the HTML imports scenario, there are some other reasons to go this way:

  1. We implemented the same logic for break on load to work for Webkit debugger inside VS. So this is a tried and tested approach.
  2. We are not aware of the performance impact of the experimental flag since we never got around to test it out. Plus it's experimental, so we can't be sure of it's implementation in the future.

So we agree that this might not be the best solution out there, but it's a safe bet for us now.
What do you think?

@auchenberg
Copy link
Contributor

@rakatyal

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.

@rakatyal
Copy link
Contributor Author

@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.

@digeff
Copy link
Contributor

digeff commented Sep 28, 2017

@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:

  • Does DOMDebugger.setInstrumentationBreakpoint has the same performance as .*:0:0?
  • How many customers have project with 100s of JavaScript files?

I don't have those answers...

@auchenberg
Copy link
Contributor

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.


const mappedUrl = this._pathTransformer.scriptParsed(pausedScriptUrl);
let normalizedMappedUrl: string;
if (mappedUrl !== undefined) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

// 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);
Copy link
Contributor

@digeff digeff Sep 29, 2017

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?

Copy link
Contributor Author

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.

Copy link
Member

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.


// 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))
Copy link
Contributor

@digeff digeff Sep 29, 2017

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

}

// If the file has unbound breakpoints, make sure to resolve them first and then decide to continue or not
if (this._pendingBreakpointsByUrl.has(normalizedMappedUrl)) {
Copy link
Contributor

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...


// 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))
Copy link
Contributor

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...

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

@rakatyal rakatyal Oct 3, 2017

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?

Copy link
Member

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.

Copy link
Member

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.

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()
Copy link
Contributor

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...

@@ -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) {
Copy link
Contributor

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?

Copy link
Member

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?


// 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
Copy link
Member

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

Copy link
Contributor Author

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.

const mappedUrl = this._pathTransformer.scriptParsed(pausedScriptUrl);
let normalizedMappedUrl: string;
if (mappedUrl !== undefined) {
normalizedMappedUrl = path.normalize(mappedUrl);
Copy link
Member

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.

Copy link
Contributor Author

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:https://folder//file while I stored the mappings for stopOnEntry dictionaries without it. So I changed to use normalized paths for consistencies.

Copy link
Member

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?

@@ -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) {
Copy link
Member

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?


// 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))
Copy link
Member

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?

// 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);
Copy link
Member

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.

.then(() => this._pendingBreakpointsByUrl.delete(source));
let normalizedSource: string;
if (source !== undefined) {
normalizedSource = path.normalize(source);
Copy link
Member

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.

Copy link
Contributor Author

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.

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);
Copy link
Member

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?

Copy link
Member

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.

@@ -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) {
Copy link
Member

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

});

// 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (script) {

Copy link
Member

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) {

const fileNameWithoutExtension = path.parse(fileNameWithoutFullPath).name;
const escapedFileName = fileNameWithoutExtension.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');

return ".*[\\\\\\/]" + escapedFileName + "([^A-z].*)?$";
Copy link
Member

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

@@ -472,6 +481,60 @@ export abstract class ChromeDebugAdapter implements IDebugAdapter {
} else if (notification.hitBreakpoints && notification.hitBreakpoints.length) {
reason = 'breakpoint';

const hitBreakpoints = notification.hitBreakpoints;
Copy link
Member

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));
Copy link
Contributor

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?

@digeff
Copy link
Contributor

digeff commented Oct 2, 2017

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...

/* 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 {
Copy link
Member

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

} catch (e) {
logger.log(`Exception occured while trying to set stop on entry breakpoint ${e.message}.`);
}
breakpointId = result.breakpointId;
Copy link
Member

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

@@ -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;
Copy link
Member

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

@@ -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;
Copy link
Member

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).

* 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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just call it resolvePendingBreakpointsOfPausedScript

@roblourens
Copy link
Member

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 BreakOnLoadHelper that holds all the state specific to this feature, and gets called into for onPaused, addBreakpoints, etc, as appropriate.

@rakatyal
Copy link
Contributor Author

@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.

@roblourens
Copy link
Member

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.

@rakatyal
Copy link
Contributor Author

Okay. I will give it a try and reach out if I need directions.

@rakatyal
Copy link
Contributor Author

@roblourens: Done some refactoring. Please have a look.

Copy link
Member

@roblourens roblourens left a 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.


// 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: '' });
Copy link
Member

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

} 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') {
Copy link
Member

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.

// 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)) {
Copy link
Member

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?

@@ -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') {
Copy link
Member

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.

/* 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 {
Copy link
Member

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

@roblourens
Copy link
Member

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?

@rakatyal
Copy link
Contributor Author

rakatyal commented Nov 1, 2017

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?

@roblourens
Copy link
Member

That sounds good to me

@roblourens
Copy link
Member

This looks great, I'll merge this once there are tests in vscode-chrome-debug

@roblourens roblourens merged commit 22fbf5f into microsoft:master Nov 28, 2017
@auchenberg
Copy link
Contributor

Woohu 🎉

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants