Skip to content

add a debugger UI #80

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

Merged
merged 25 commits into from
Jan 3, 2019
Merged

add a debugger UI #80

merged 25 commits into from
Jan 3, 2019

Conversation

devoncarew
Copy link
Member

This adds a debugging UI to devtools. It:

  • displays the call stack, variables, breakpoints, and the app's scripts
  • allows the user to set and clear breakpoints
  • supports resuming execution when paused and stepping in/out/over
  • supports async stepping and displaying async frames
  • uses codemirror to display the app source
  • displays console output in-line in the page
  • calls toString() on objects in the variables view on mouse hover
  • has tests for much of the above

screen shot 2019-01-02 at 11 17 22 am

This PR is ready for review. We will want to (perhaps out of band of the PR), decide whether we do want a debugger UI in devtools, whether we should have it enabled now or only accessible through a compile time flag or similar. Happy to take feedback about how this fits into the product here or offline.

@jacob314 @kenzieschmoll

/* found in the LICENSE file. */

.debugger-menu {
width: 280px;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: does this need to be hard coded to 280px?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, currently. It's the width of the left sections in the debugger UI. Our options are hard-coding a width, letting it float based on a percentage of the screen width, or building a user-positionable splitter control.

Of the 3 I prefer a hard-coded width. It ensures that the contents are still readable in more narrow windows, and doesn't get too wide for wider windows.

When designing this ui, I think we can assume a browser width of ~1300px (https://www.websitedimensions.com/).

Long term we'll likely want some user positionable controls, but likely not an immediate concern.

final PButton resumeButton = new PButton(null)
..primary()
..small()
..element.style.minWidth = '90px'
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason this style shouldn't be in the css?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, I'll move this css.

stepIn.click(() => debuggerState.stepIn());
stepOut.click(() => debuggerState.stepOut());

final Map<String, dynamic> options = <String, dynamic>{
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: consider omitting they types on the RHS unless they are adding value. we aren't forcing the flutter types everywhere style for this code anymore

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 will do; I started this PR before the style change

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW I think it's nicer to omit types on the left. If the generic arguments are needed (they could be skipped altogether here) put them on the literal only.

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if we had a dart quick fix that stripped out all types that are inferred.

Copy link
Member Author

@devoncarew devoncarew Jan 3, 2019

Choose a reason for hiding this comment

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

We'll want to hew closely to the flutter/flutter style in this repo.

final CodeMirror codeMirror =
new CodeMirror.fromElement(sourceArea.element, options: options);
codeMirror.setReadOnly(true);
// ignore: always_specify_types
Copy link
Contributor

Choose a reason for hiding this comment

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

this ignore: is not needed anymore as we switched the lint.

final Frame frame = frames.first;

final Frame newFrame = new Frame();
newFrame.type = frame.type;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: cascades are nice for this case. also consider adding clone constructor to Frame.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 re: cascades

The ctor is defined in a package: we're importing, but I'll open an issue to improve the options for constructors for these classes.

deviceStatus.element.text = '';

scriptsView.clearScripts();
_scriptCountDiv.text = '0';
Copy link
Contributor

Choose a reason for hiding this comment

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

I see setting the state of _scriptCountDiv a lot of places. Consider adding a helper that syncs its value to the current script count.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

Copy link
Member Author

Choose a reason for hiding this comment

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

changed to listening to script changes, and having the listener update the count

scriptPrefix =
scriptPrefix.substring(0, scriptPrefix.lastIndexOf('/') + 1);
}
} else if (scriptPrefix.contains('/example/')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this hard coding of /example/ needed? Seems a bit surprising. Generally it would be helpful to move this logic for determining the root directory to a shared util as I also need it for the inspector to clarify what directories are considered the local project for highlighting only user created widgets.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is tough as we don't have a fool-proof way to determine what the root of a project is w/ just the info from the service protocol.

For flutter apps, they generally (always?) run as package:my_app urls (the first case above), so I think we'll be able to determine app code vs other code fairly reliably.

For cli apps, they generally run as file: urls, and have a bit more ambiguity about what is app code. In the logic above, I'm trying to walkup the url a bit. and use package layout conventions to identify the app directory.

I'll trim the logic for example/ above as its less common than lib/ or bin/.

}
}

class Pos {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Pos --> Position?

Copy link
Member Author

Choose a reason for hiding this comment

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

That conflicts with an import, but I can find a better name than Pos.

Copy link
Member Author

Choose a reason for hiding this comment

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

SourcePosition

_items = new SelectableList<Breakpoint>()
..flex()
..clazz('menu-item-bottom-border')
..element.style.overflowY = 'scroll';
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: specify overflowY with CSS file?

Copy link
Member Author

Choose a reason for hiding this comment

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

done

_items.setRenderer((Breakpoint breakpoint) {
final dynamic location = breakpoint.location;

final CoreElement element = li(text: '', c: 'list-item');
Copy link
Contributor

Choose a reason for hiding this comment

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

can text parameter be omitted as it is empty?

Copy link
Member Author

Choose a reason for hiding this comment

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

yup, done


Stream<Breakpoint> get onDoubleClick => _items.onDoubleClick;

final CoreElement _breakpointsCountDiv;
Copy link
Contributor

Choose a reason for hiding this comment

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

consider moving all fields up closer to the top of the class.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

String uri1 = ref1.uri;
String uri2 = ref2.uri;

if (uri1.startsWith('dart:_')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

add a helper method to cleanup dart:_ uris

Copy link
Member Author

Choose a reason for hiding this comment

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

done

_items = new SelectableList<Frame>()
..flex()
..clazz('menu-item-bottom-border')
..element.style.overflowY = 'scroll';
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like all the menus allow scroll. maybe add that to the css file?

Copy link
Member Author

Choose a reason for hiding this comment

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

done

}

class ScriptAndPos {
ScriptAndPos(this.uri, {this.position});
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add @required to position argument assuming it is required.

Copy link
Member Author

Choose a reason for hiding this comment

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

done

ScriptRef getRef(dynamic location) {
if (location is SourceLocation) {
return location.script;
} else if (location is UnresolvedSourceLocation) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit :
no need for either else as each if clause ends in a return.


int getPos(dynamic location) {
if (location is SourceLocation) {
return location.tokenPos ?? 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: make location.tokenPos be made non-nullable instead of having a check here?

Copy link
Member Author

Choose a reason for hiding this comment

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

We get this info directly from the service protocol - in some cases tokenPos can be null.

ConsoleArea() {
final Map<String, dynamic> options = <String, dynamic>{
'mode': 'text/plain',
//'lineNumbers': true,
Copy link
Contributor

Choose a reason for hiding this comment

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

remove commented line or add it as an option to ConsoleArea.

Copy link
Member Author

Choose a reason for hiding this comment

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

removed

_editor.scrollIntoView(lastLineIndex, lastLine.length);
}

// for testing
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

done

Copy link
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

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

This looks great! Lgtm with comments.

description = 'items: $count';

final List<int> numbers = new List.generate(10, (index) => index * index);
// ignore: unused_local_variable
Copy link
Member

Choose a reason for hiding this comment

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

nit: unnecessary ignore


if (value is Sentinel) {
return value.valueAsString;
} else if (value is TypeArgumentsRef) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: else's not needed since every if statement returns.

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

if (result is Instance) {
final Instance obj = result;
return obj.valueAsString;
} else {
Copy link
Member

Choose a reason for hiding this comment

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

nit: else not needed since the if statement returns.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like the parallelism here since there are just two branches.

Future<Success> stepOut() =>
service.resume(isolateRef.id, step: StepOption.kOut);

// Used for testing.
Copy link
Member

Choose a reason for hiding this comment

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

nit: use visibleForTesting

Copy link
Member Author

Choose a reason for hiding this comment

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

done

return service.addBreakpoint(isolateRef.id, scriptId, line);
}

// Used for testing.
Copy link
Member

Choose a reason for hiding this comment

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

nit: visibleForTesting

Copy link
Member Author

Choose a reason for hiding this comment

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

done

@devoncarew
Copy link
Member Author

Thanks for the comments! I'm going to get the tests working, work through the comments, and then ping this PR.

@devoncarew
Copy link
Member Author

The tests look like they have race conditions when run on travis unfortunately.

Uncaught, null, 18, 0, object null
  package:webkit_inspection_protocol/src/runtime.dart 23:7  WipRuntime.evaluate
  ===== asynchronous gap ===========================
  dart:async                                                _AsyncAwaitCompleter.completeError
  package:webkit_inspection_protocol/src/runtime.dart       WipRuntime.evaluate
  ===== asynchronous gap ===========================
  dart:async                                                _asyncThenWrapperHelper
  package:webkit_inspection_protocol/src/runtime.dart       WipRuntime.evaluate
  test/integration_test.dart 569:38                         BrowserTabInstance._getAppChannelObject
  test/integration_test.dart 558:22                         BrowserTabInstance.getBrowserChannel
  ===== asynchronous gap ===========================
  dart:async                                                _asyncThenWrapperHelper
  test/integration_test.dart                                BrowserTabInstance.getBrowserChannel
  test/integration_test.dart 377:23                         DevtoolsManager.start
  ===== asynchronous gap ===========================
  dart:async                                                _asyncThenWrapperHelper
  test/integration_test.dart                                DevtoolsManager.start
  test/integration_test.dart 117:17                         debuggingTests.<fn>
  ===== asynchronous gap ===========================
  dart:async                                                _asyncThenWrapperHelper
  test/integration_test.dart                                debuggingTests.<fn>

Perhaps just adding a wait or a condition at the right place?

stepIn.click(() => debuggerState.stepIn());
stepOut.click(() => debuggerState.stepOut());

final Map<String, dynamic> options = <String, dynamic>{
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW I think it's nicer to omit types on the left. If the generic arguments are needed (they could be skipped altogether here) put them on the literal only.

}

class DebuggerState {
DebuggerState();
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] omit

Copy link
Member Author

Choose a reason for hiding this comment

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

done

Copy link
Member Author

Choose a reason for hiding this comment

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

this would likely make a good lint...


List<Breakpoint> get breakpoints => _breakpoints.value;

void setVmService(VmService service) {
Copy link
Contributor

Choose a reason for hiding this comment

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

how would the user of this API know to use this instead of the setter directly?

How bout making the field private with a public set and get to attach the extra behavior here.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is an internal class, so not intended to be driven from outside the library.

I like the explicit setFoo here, as this does do a bit of work. +1 to making the field private; this method is the only way that people outside of the class should manipulate the value.

commonScriptPrefix = scriptPrefix;
}

String getShortScriptName(String uri) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure that applies, as this method takes a param?

Copy link
Contributor

Choose a reason for hiding this comment

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

From the guide: "Even if the member does need to be a method because it takes arguments or otherwise isn’t a good fit for a getter, you should still avoid get."


void main() {
const Duration delay = Duration(seconds: 4);
Function work;
Copy link
Contributor

Choose a reason for hiding this comment

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

this is only assigned once, should it be written as a function instead of assigning a lambda?

https://www.dartlang.org/guides/language/effective-dart/usage#do-use-a-function-declaration-to-bind-a-function-to-a-name

Copy link
Member Author

Choose a reason for hiding this comment

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

I use this script for manual testing of the debugger, so it's got some oddities. I can look at using a class instead of top-level functions in order to get the same behavior I need.

Copy link
Contributor

Choose a reason for hiding this comment

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

The function does not need to be top level, it can be defined within main. From what I can see you can use exactly what you have if you:

  • Remove variable declaration
  • Remove the = at the definition point
  • Remove the semicolon following the last }

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, when you were testing did you specifically need a variable that held a closure? If that's the case maybe add a comment?

@@ -0,0 +1,83 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: for all new files update the year to 2019.

Copy link
Member Author

Choose a reason for hiding this comment

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

will do

Copy link
Member Author

@devoncarew devoncarew left a comment

Choose a reason for hiding this comment

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

I made a pass through the comments, and w/ the updated build from a separate PR, believe I have the tests running on the bots.

/* found in the LICENSE file. */

.debugger-menu {
width: 280px;
Copy link
Member Author

Choose a reason for hiding this comment

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

Well, currently. It's the width of the left sections in the debugger UI. Our options are hard-coding a width, letting it float based on a percentage of the screen width, or building a user-positionable splitter control.

Of the 3 I prefer a hard-coded width. It ensures that the contents are still readable in more narrow windows, and doesn't get too wide for wider windows.

When designing this ui, I think we can assume a browser width of ~1300px (https://www.websitedimensions.com/).

Long term we'll likely want some user positionable controls, but likely not an immediate concern.

final PButton resumeButton = new PButton(null)
..primary()
..small()
..element.style.minWidth = '90px'
Copy link
Member Author

Choose a reason for hiding this comment

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

No, I'll move this css.

stepIn.click(() => debuggerState.stepIn());
stepOut.click(() => debuggerState.stepOut());

final Map<String, dynamic> options = <String, dynamic>{
Copy link
Member Author

@devoncarew devoncarew Jan 3, 2019

Choose a reason for hiding this comment

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

We'll want to hew closely to the flutter/flutter style in this repo.

final Frame frame = frames.first;

final Frame newFrame = new Frame();
newFrame.type = frame.type;
Copy link
Member Author

Choose a reason for hiding this comment

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

👍 re: cascades

The ctor is defined in a package: we're importing, but I'll open an issue to improve the options for constructors for these classes.


if (value is Sentinel) {
return value.valueAsString;
} else if (value is TypeArgumentsRef) {
Copy link
Member Author

Choose a reason for hiding this comment

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

updated


int getPos(dynamic location) {
if (location is SourceLocation) {
return location.tokenPos ?? 0;
Copy link
Member Author

Choose a reason for hiding this comment

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

We get this info directly from the service protocol - in some cases tokenPos can be null.

ConsoleArea() {
final Map<String, dynamic> options = <String, dynamic>{
'mode': 'text/plain',
//'lineNumbers': true,
Copy link
Member Author

Choose a reason for hiding this comment

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

removed

_editor.scrollIntoView(lastLineIndex, lastLine.length);
}

// for testing
Copy link
Member Author

Choose a reason for hiding this comment

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

done

@@ -0,0 +1,83 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
Copy link
Member Author

Choose a reason for hiding this comment

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

will do


void main() {
const Duration delay = Duration(seconds: 4);
Function work;
Copy link
Member Author

Choose a reason for hiding this comment

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

I use this script for manual testing of the debugger, so it's got some oddities. I can look at using a class instead of top-level functions in order to get the same behavior I need.

@devoncarew devoncarew merged commit be2b798 into flutter:master Jan 3, 2019
@@ -4,13 +4,15 @@ version: 0.0.1
homepage: https://github.com/flutter/devtools

environment:
sdk: '>=2.0.0-dev <3.0.0'
sdk: '>=2.1.0 <3.0.0'
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW - this constraint prevents the SDK included in Flutter v1.0 from being able to activate this package (it shipped with 2.1.0-dev.9.4.flutter-f9ebf21297 and a -dev suffix makes that "less than" 2.1.0).

I suspect we're not concerned but since I noticed it when adding DC integration tests and it's a little counter-intuitive (we consider Flutter v1.0 shipped with Dart 2.1, but semver doesn't see it that way!) I thought it worth noting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since we're effectively just using this constraint in the pub global activated server app, I think we can change the lower bound to Flutter's.

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

Successfully merging this pull request may close these issues.

5 participants