Skip to content
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

Implementing break on load #241

Merged
merged 6 commits into from Nov 28, 2017
Merged

Implementing break on load #241

merged 6 commits into from Nov 28, 2017

Conversation

@rakatyal
Copy link
Member

rakatyal commented Sep 27, 2017

No description provided.

@auchenberg

This comment has been minimized.

Copy link
Contributor

auchenberg commented Sep 27, 2017

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

microsoft/vscode-chrome-debug#445

@roblourens

This comment has been minimized.

Copy link
Member

roblourens commented Sep 27, 2017

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

@auchenberg

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member Author

rakatyal commented Sep 27, 2017

@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

This comment has been minimized.

Copy link
Contributor

auchenberg commented Sep 27, 2017

@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

This comment has been minimized.

Copy link
Member Author

rakatyal commented Sep 27, 2017

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Contributor

auchenberg commented Sep 28, 2017

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

Could you please describe in which scenario will **mappedUrl ** be undefined, and what the code will do in that scenario?

This comment has been minimized.

Copy link
@rakatyal

rakatyal Sep 29, 2017

Author Member

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

Should we also show a message to the user here?

This comment has been minimized.

Copy link
@rakatyal

rakatyal Sep 29, 2017

Author Member

Hmm I am unsure about that one. @roblourens What do you think? I guess if this happens, Chrome will be stuck in paused state.

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

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

This comment has been minimized.

Copy link
@rakatyal

rakatyal Sep 29, 2017

Author Member

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?

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

I don't understand the flag, don't you need to keep this info per-file?

This comment has been minimized.

Copy link
@rakatyal

rakatyal Oct 3, 2017

Author Member

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?

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 4, 2017

Member

So it relies on the scriptParsed and onPause happening right next to each other, and scriptParsed coming first? That's probably a safe assumption.

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 4, 2017

Member

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

@roblourens Given that Chrome tends to ignore column numbers, is this the best check to do here?

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

I don't understand the index.js/index.bin.js example

This comment has been minimized.

Copy link
@rakatyal

rakatyal Oct 3, 2017

Author Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

Why do you need path.normalize? I don't use that anywhere else.

This comment has been minimized.

Copy link
@rakatyal

rakatyal Oct 3, 2017

Author Member

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.

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 3, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

Don't use path.normalize without a reason, e.g. these can be URLs or remote paths.

This comment has been minimized.

Copy link
@rakatyal

rakatyal Oct 3, 2017

Author Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

This should happen in either case, not just break on load, right?

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

if (script) {

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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].*)?$";

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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;

This comment has been minimized.

Copy link
@roblourens

roblourens Sep 29, 2017

Member

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

This comment has been minimized.

Copy link
@digeff

digeff Sep 29, 2017

Contributor

Isn't there a better way to do this?

@digeff

This comment has been minimized.

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

@rakatyal rakatyal force-pushed the rakatyal:breakonload branch from 70217ca to 93062b1 Oct 23, 2017
/* 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 {

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 24, 2017

Member

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;

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 24, 2017

Member

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;

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 24, 2017

Member

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;

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 24, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 24, 2017

Member

Just call it resolvePendingBreakpointsOfPausedScript

@roblourens

This comment has been minimized.

Copy link
Member

roblourens commented Oct 24, 2017

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

This comment has been minimized.

Copy link
Member Author

rakatyal commented Oct 25, 2017

@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

This comment has been minimized.

Copy link
Member

roblourens commented Oct 25, 2017

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

This comment has been minimized.

Copy link
Member Author

rakatyal commented Oct 25, 2017

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

@rakatyal rakatyal force-pushed the rakatyal:breakonload branch from 9fcf773 to 330568e Oct 30, 2017
@rakatyal

This comment has been minimized.

Copy link
Member Author

rakatyal commented Oct 30, 2017

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

Copy link
Member

roblourens left a comment

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: '' });

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 31, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 31, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 31, 2017

Member

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

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 31, 2017

Member

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 {

This comment has been minimized.

Copy link
@roblourens

roblourens Oct 31, 2017

Member

Move to chromeUtils and write unit tests

@roblourens

This comment has been minimized.

Copy link
Member

roblourens commented Oct 31, 2017

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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Member

roblourens commented Nov 1, 2017

That sounds good to me

@roblourens

This comment has been minimized.

Copy link
Member

roblourens commented Nov 4, 2017

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
3 checks passed
3 checks passed
continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
license/cla All CLA requirements met.
Details
@auchenberg

This comment has been minimized.

Copy link
Contributor

auchenberg commented Nov 28, 2017

Woohu 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants
You can’t perform that action at this time.