Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1146)

Unified Diff: runtime/observatory/lib/src/elements/debugger.dart

Issue 866663003: Add a working command line debugger to Observatory. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: runtime/observatory/lib/src/elements/debugger.dart
diff --git a/runtime/observatory/lib/src/elements/debugger.dart b/runtime/observatory/lib/src/elements/debugger.dart
index f17bacb4a7a12f5d7e89b93e9439745c4848004b..9c18f941245e7b0657c7354a6ae4eea5ce9ff922 100644
--- a/runtime/observatory/lib/src/elements/debugger.dart
+++ b/runtime/observatory/lib/src/elements/debugger.dart
@@ -4,15 +4,431 @@
library debugger_page_element;
+import 'dart:async';
import 'dart:html';
import 'observatory_element.dart';
+import 'package:observatory/cli.dart';
import 'package:observatory/service.dart';
import 'package:polymer/polymer.dart';
+// TODO(turnidge): Move Debugger, DebuggerCommand to their own lib.
+abstract class DebuggerCommand extends Command {
+ Debugger debugger;
+
+ DebuggerCommand(this.debugger, name, children)
+ : super(name, children);
+}
+
+class HelpCommand extends DebuggerCommand {
+ HelpCommand(Debugger debugger) : super(debugger, 'help', []);
+
+ Future run(List<String> args) {
+ var con = debugger.console;
+ con.printLine('List of commands:');
+ con.newline();
+
+ // TODO(turnidge): Build a real help system.
+ List completions = debugger.cmd.completeCommand('');
+ completions = completions.map((s )=> s.trimRight()).toList();
+ completions.sort();
+ con.printLine(completions.toString());
+ con.newline();
+ con.printLine("Command prefixes are accepted (e.g. 'h' for 'help')");
+ con.printLine("Hit [TAB] to complete a command (try 'i[TAB][TAB]')");
+ con.printLine("Hit [ENTER] to repeat the last command");
+
+ return new Future.value(null);
+ }
+}
+
+class PauseCommand extends DebuggerCommand {
+ PauseCommand(Debugger debugger) : super(debugger, 'pause', []);
+
+ Future run(List<String> args) {
+ if (!debugger.isolatePaused()) {
+ return debugger.isolate.pause();
+ } else {
+ debugger.console.printLine('The program is already paused');
+ return new Future.value(null);
+ }
+ }
+}
+
+class ContinueCommand extends DebuggerCommand {
+ ContinueCommand(Debugger debugger) : super(debugger, 'continue', []);
+
+ Future run(List<String> args) {
+ if (debugger.isolatePaused()) {
+ return debugger.isolate.resume().then((_) {
+ debugger.warnOutOfDate();
+ });
+ } else {
+ debugger.console.printLine('The program must be paused');
+ return new Future.value(null);
+ }
+ }
+}
+
+class NextCommand extends DebuggerCommand {
+ NextCommand(Debugger debugger) : super(debugger, 'next', []);
+
+ Future run(List<String> args) {
+ if (debugger.isolatePaused()) {
+ var event = debugger.isolate.pauseEvent;
+ if (event.eventType == 'IsolateCreated') {
+ debugger.console.printLine("Type 'continue' to start the isolate");
+ return new Future.value(null);
+ }
+ if (event.eventType == 'IsolateShutdown') {
+ debugger.console.printLine("Type 'continue' to exit the isolate");
+ return new Future.value(null);
+ }
+ return debugger.isolate.stepOver();
+ } else {
+ debugger.console.printLine('The program is already running');
+ return new Future.value(null);
+ }
+ }
+}
+
+class StepCommand extends DebuggerCommand {
+ StepCommand(Debugger debugger) : super(debugger, 'step', []);
+
+ Future run(List<String> args) {
+ if (debugger.isolatePaused()) {
+ var event = debugger.isolate.pauseEvent;
+ if (event.eventType == 'IsolateCreated') {
+ debugger.console.printLine("Type 'continue' to start the isolate");
+ return new Future.value(null);
+ }
+ if (event.eventType == 'IsolateShutdown') {
+ debugger.console.printLine("Type 'continue' to exit the isolate");
+ return new Future.value(null);
+ }
+ return debugger.isolate.stepInto();
+ } else {
+ debugger.console.printLine('The program is already running');
+ return new Future.value(null);
+ }
+ }
+}
+
+class FinishCommand extends DebuggerCommand {
+ FinishCommand(Debugger debugger) : super(debugger, 'finish', []);
+
+ Future run(List<String> args) {
+ if (debugger.isolatePaused()) {
+ return debugger.isolate.stepOut();
+ } else {
+ debugger.console.printLine('The program is already running');
+ return new Future.value(null);
+ }
+ }
+}
+
+// TODO(turnidge): Add argument completion.
+class DeleteCommand extends DebuggerCommand {
+ DeleteCommand(Debugger debugger) : super(debugger, 'delete', []);
+
+ Future run(List<String> args) {
+ if (args.length < 1) {
+ debugger.console.printLine('delete expects one or more arguments');
+ return new Future.value(null);
+ }
+ List toDelete = [];
+ for (var arg in args) {
+ int id = int.parse(arg);
+ var bpt = null;
+ for (var candidate in debugger.isolate.breakpoints) {
+ if (candidate['breakpointNumber'] == id) {
+ bpt = candidate;
+ break;
+ }
+ }
+ if (bpt == null) {
+ debugger.console.printLine("Invalid breakpoint id '${id}'");
+ return new Future.value(null);
+ }
+ toDelete.add(bpt);
+ }
+ List pending = [];
+ for (var bpt in toDelete) {
+ pending.add(debugger.isolate.removeBreakpoint(bpt).then((_) {
+ var id = bpt['breakpointNumber'];
+ debugger.console.printLine("Removed breakpoint $id");
+ }));
+ }
+ return Future.wait(pending);
+ }
+}
+
+class InfoBreakpointsCommand extends DebuggerCommand {
+ InfoBreakpointsCommand(Debugger debugger)
+ : super(debugger, 'breakpoints', []);
+
+ Future run(List<String> args) {
+ return debugger.isolate.reloadBreakpoints().then((_) {
+ if (debugger.isolate.breakpoints.isEmpty) {
+ debugger.console.printLine('No breakpoints');
+ }
+ for (var bpt in debugger.isolate.breakpoints) {
+ var bpId = bpt['breakpointNumber'];
+ var script = bpt['location']['script'];
+ var tokenPos = bpt['location']['tokenPos'];
+ var line = script.tokenToLine(tokenPos);
+ var col = script.tokenToCol(tokenPos);
+ debugger.console.printLine(
+ 'Breakpoint ${bpId} at ${script.name}:${line}:${col}');
+ }
+ });
+ }
+}
+
+class InfoIsolatesCommand extends DebuggerCommand {
+ InfoIsolatesCommand(Debugger debugger) : super(debugger, 'isolates', []);
+
+ Future run(List<String> args) {
+ for (var isolate in debugger.isolate.vm.isolates) {
+ debugger.console.printLine(
+ "Isolate ${isolate.id} '${isolate.name}'");
+ }
+ return new Future.value(null);
+ }
+}
+
+class InfoCommand extends DebuggerCommand {
+ InfoCommand(Debugger debugger) : super(debugger, 'info', [
+ new InfoBreakpointsCommand(debugger),
+ new InfoIsolatesCommand(debugger),
+ ]);
+
+ Future run(List<String> args) {
+ debugger.console.printLine("Invalid info command");
+ return new Future.value(null);
+ }
+}
+
+class RefreshCoverageCommand extends DebuggerCommand {
+ RefreshCoverageCommand(Debugger debugger) : super(debugger, 'coverage', []);
+
+ Future run(List<String> args) {
+ Set<Script> scripts = debugger.stackElement.activeScripts();
+ List pending = [];
+ for (var script in scripts) {
+ pending.add(script.refreshCoverage().then((_) {
+ debugger.console.printLine('Refreshed coverage for ${script.name}');
+ }));
+ }
+ return Future.wait(pending);
+ }
+}
+
+class RefreshCommand extends DebuggerCommand {
+ RefreshCommand(Debugger debugger) : super(debugger, 'refresh', [
+ new RefreshCoverageCommand(debugger),
+ ]);
+
+ Future run(List<String> args) {
+ return debugger.refreshStack();
+ }
+}
+
+// Tracks the state for an isolate debugging session.
+class Debugger {
+ RootCommand cmd;
+ DebuggerConsoleElement console;
+ DebuggerStackElement stackElement;
+ ServiceMap stack;
+
+ Debugger() {
+ cmd = new RootCommand([
+ new HelpCommand(this),
+ new PauseCommand(this),
+ new ContinueCommand(this),
+ new NextCommand(this),
+ new StepCommand(this),
+ new FinishCommand(this),
+ new DeleteCommand(this),
+ new InfoCommand(this),
+ new RefreshCommand(this),
+ ]);
+ }
+
+ void set isolate(Isolate iso) {
+ _isolate = iso;
+ if (_isolate != null) {
+ _isolate.reload().then((_) {
+ _isolate.vm.events.stream.listen(_onEvent);
+ _refreshStack(isolate.pauseEvent).then((_) {
+ reportStatus();
+ });
+ });
+ }
+ }
+ Isolate get isolate => _isolate;
+ Isolate _isolate;
+
+ void init() {
+ console.newline();
+ console.printBold("Type 'h' for help");
+ }
+
+ Future refreshStack() {
+ return _refreshStack(isolate.pauseEvent).then((_) {
+ reportStatus();
+ });
+ }
+
+ bool isolatePaused() {
+ // TODO(turnidge): Stop relying on the isolate to track the last
+ // pause event. Since we listen to events directly in the
+ // debugger, this could introduce a race.
+ return isolate.pauseEvent != null;
+ }
+
+ void warnOutOfDate() {
+ // Wait a bit, then tell the user that the stack may be out of date.
+ new Timer(const Duration(seconds:2), () {
+ if (!isolatePaused()) {
+ stackElement.isSampled = true;
+ }
+ });
+ }
+
+ Future<ServiceMap> _refreshStack(ServiceEvent pauseEvent) {
+ return isolate.getStack().then((result) {
+ stack = result;
+ // TODO(turnidge): Replace only the changed part of the stack to
+ // reduce flicker.
+ // stackElement.stack = stack;
+ stackElement.updateStack(stack, pauseEvent);
+ });
+ }
+
+ void reportStatus() {
+ if (_isolate.idle) {
+ console.printLine('Isolate is idle');
+ } else if (_isolate.running) {
+ console.printLine("Isolate is running (type 'pause' to interrupt)");
+ } else if (_isolate.pauseEvent != null) {
+ _reportPause(_isolate.pauseEvent);
+ } else {
+ console.printLine('Isolate is in unknown state');
+ }
+ }
+
+ void _reportPause(ServiceEvent event) {
+ if (event.eventType == 'IsolateCreated') {
+ console.printLine(
+ "Paused at isolate start (type 'continue' to start the isolate')");
+ } else if (event.eventType == 'IsolateShutdown') {
+ console.printLine(
+ "Paused at isolate exit (type 'continue' to exit the isolate')");
+ }
+ if (stack['frames'].length > 0) {
+ var frame = stack['frames'][0];
+ var script = frame['script'];
+ script.load().then((_) {
+ var line = script.tokenToLine(frame['tokenPos']);
+ var col = script.tokenToCol(frame['tokenPos']);
+ if (event.breakpoint != null) {
+ var bpId = event.breakpoint['breakpointNumber'];
+ console.printLine('Breakpoint ${bpId} at ${script.name}:${line}:${col}');
+ } else if (event.exception != null) {
+ // TODO(turnidge): Test this.
+ console.printLine(
+ 'Exception ${event.exception} at ${script.name}:${line}:${col}');
+ } else {
+ console.printLine('Paused at ${script.name}:${line}:${col}');
+ }
+ });
+ }
+ }
+
+ void _onEvent(ServiceEvent event) {
+ if (event.owner != isolate) {
+ return;
+ }
+ switch(event.eventType) {
+ case 'IsolateShutdown':
+ console.printLine('Isolate shutdown');
+ isolate = null;
+ break;
+
+ case 'BreakpointReached':
+ case 'IsolateInterrupted':
+ case 'ExceptionThrown':
+ _refreshStack(event).then((_) {
+ _reportPause(event);
+ });
+ break;
+
+ case 'IsolateResumed':
+ console.printLine('Continuing...');
+ break;
+
+ case '_Graph':
+ case 'BreakpointResolved':
+ case 'IsolateCreated':
+ case 'GC':
+ // Ignore these events for now.
+ break;
+
+ default:
+ console.printLine('Unrecognized event: $event');
+ break;
+ }
+ }
+
+ String complete(String line) {
+ List<String> completions = cmd.completeCommand(line);
+ if (completions.length == 0) {
+ // No completions. Leave the line alone.
+ return line;
+ } else if (completions.length == 1) {
+ // Unambiguous completion.
+ return completions[0];
+ } else {
+ // Ambigous completion.
+ completions = completions.map((s )=> s.trimRight()).toList();
+ completions.sort();
+ console.printBold(completions.toString());
+
+ // TODO(turnidge): Complete to common prefix of all completions.
+ return line;
+ }
+ }
+
+ // TODO(turnidge): Implement real command line history.
+ String lastCommand;
+ bool busy = false;
+
+ Future run(String command) {
+ assert(!busy);
+ busy = true;
+ if (command == '') {
+ command = lastCommand;
+ }
+ lastCommand = command;
+ console.printBold('\$ $command');
+ return cmd.runCommand(command).then((_) {
+ busy = false;
+ }).catchError((e) {
+ console.printLine('ERROR $e');
+ });
+ }
+}
+
@CustomTag('debugger-page')
class DebuggerPageElement extends ObservatoryElement {
@published Isolate isolate;
- @published bool showConsole = false;
+
+ isolateChanged(oldValue) {
+ if (isolate != null) {
+ debugger.isolate = isolate;
+ }
+ }
+ Debugger debugger = new Debugger();
DebuggerPageElement.created() : super.created();
@@ -20,34 +436,127 @@ class DebuggerPageElement extends ObservatoryElement {
void attached() {
super.attached();
- // TODO(turnidge): Get these values from the DOM.
- // TODO(turnidge): splitterHeight is 0 until I implement it.
- const int navbarHeight = 56;
- const int splitterHeight = 0;
- const int cmdHeight = 22;
+ var navbarDiv = $['navbarDiv'];
+ var stackDiv = $['stackDiv'];
+ var splitterDiv = $['splitterDiv'];
+ var cmdDiv = $['commandDiv'];
+ var consoleDiv = $['consoleDiv'];
+
+ int navbarHeight = navbarDiv.clientHeight;
+ int splitterHeight = splitterDiv.clientHeight;
+ int cmdHeight = cmdDiv.clientHeight;
- var stack = $['stack'];
int windowHeight = window.innerHeight;
- int available = windowHeight - (navbarHeight + splitterHeight);
- int stackHeight = available ~/ 1.3;
- if (showConsole) {
- stack.style.setProperty('height', '${stackHeight}px');
- } else {
- stack.style.setProperty('height', '${available}px');
- }
+ int fixedHeight = navbarHeight + splitterHeight + cmdHeight;
+ int available = windowHeight - fixedHeight;
+ int stackHeight = available ~/ 1.6;
+ stackDiv.style.setProperty('height', '${stackHeight}px');
+
+ // Wire the debugger object to the stack, console, and command line.
+ var stackElement = $['stackElement'];
+ debugger.stackElement = stackElement;
+ stackElement.debugger = debugger;
+ debugger.console = $['console'];
+ $['commandline'].debugger = debugger;
+ debugger.init();
}
+
}
@CustomTag('debugger-stack')
class DebuggerStackElement extends ObservatoryElement {
@published Isolate isolate;
- @published ServiceMap stack;
- @published int activeFrame = 0;
+ @observable bool hasStack = false;
+ @observable bool isSampled = false;
+ Debugger debugger = null;
- isolateChanged(oldValue) {
- isolate.getStack().then((result) {
- stack = result;
- });
+ _addFrame(List frameList, ObservableMap frameInfo, bool expand) {
+ DebuggerFrameElement frameElement = new Element.tag('debugger-frame');
+ frameElement.expand = expand;
+ frameElement.frame = frameInfo;
+
+ var li = new LIElement();
+ li.classes.add('list-group-item');
+ li.children.insert(0, frameElement);
+
+ frameList.insert(0, li);
+ }
+
+ void updateStack(ServiceMap newStack, ServiceEvent pauseEvent) {
+ List frameElements = $['frameList'].children;
+ List newFrames = newStack['frames'];
+
+ // Remove any frames whose functions don't match, starting from
+ // bottom of stack.
+ int oldPos = frameElements.length - 1;
+ int newPos = newFrames.length - 1;
+ while (oldPos >= 0 && newPos >= 0) {
+ if (!frameElements[oldPos].children[0].matchFrame(newFrames[newPos])) {
+ // The rest of the frame elements no longer match. Remove them.
+ for (int i = 0; i <= oldPos; i++) {
+ // NOTE(turnidge): removeRange is missing, sadly.
+ frameElements.removeAt(0);
+ }
+ break;
+ }
+ oldPos--;
+ newPos--;
+ }
+
+ // Remove any extra frames.
+ if (frameElements.length > newFrames.length) {
+ // Remove old frames from the top of stack.
+ int removeCount = frameElements.length - newFrames.length;
+ for (int i = 0; i < removeCount; i++) {
+ frameElements.removeAt(0);
+ }
+ }
+
+ // Add any new frames.
+ int newCount = 0;
+ if (frameElements.length < newFrames.length) {
+ // Add new frames to the top of stack.
+ newCount = newFrames.length - frameElements.length;
+ for (int i = newCount-1; i >= 0; i--) {
+ _addFrame(frameElements, newFrames[i], i == 0);
+ }
+ }
+ assert(frameElements.length == newFrames.length);
+
+ if (frameElements.isNotEmpty) {
+ frameElements[0].children[0].expand = true;
+ for (int i = newCount; i < frameElements.length; i++) {
+ frameElements[i].children[0].updateFrame(newFrames[i]);
+ }
+ }
+
+ isSampled = pauseEvent == null;
+ hasStack = frameElements.isNotEmpty;
+ }
+
+ Set<Script> activeScripts() {
+ var s = new Set<Script>();
+ List frameElements = $['frameList'].children;
+ for (var frameElement in frameElements) {
+ s.add(frameElement.children[0].script);
+ }
+ return s;
+ }
+
+ doPauseIsolate(_) {
+ if (debugger != null) {
+ return debugger.isolate.pause();
+ } else {
+ return new Future.value(null);
+ }
+ }
+
+ doRefreshStack(_) {
+ if (debugger != null) {
+ return debugger.refreshStack();
+ } else {
+ return new Future.value(null);
+ }
}
DebuggerStackElement.created() : super.created();
@@ -66,6 +575,19 @@ class DebuggerFrameElement extends ObservatoryElement {
DebuggerFrameElement.created() : super.created();
+ bool matchFrame(ObservableMap newFrame) {
+ return newFrame['function'].id == frame['function'].id;
+ }
+
+ void updateFrame(ObservableMap newFrame) {
+ assert(matchFrame(newFrame));
+ frame['depth'] = newFrame['depth'];
+ frame['tokenPos'] = newFrame['tokenPos'];
+ frame['vars'] = newFrame['vars'];
+ }
+
+ Script get script => frame['script'];
+
@override
void attached() {
super.attached();
@@ -102,12 +624,35 @@ class DebuggerConsoleElement extends ObservatoryElement {
@published Isolate isolate;
DebuggerConsoleElement.created() : super.created();
+
+ void printLine(String line) {
+ var div = new DivElement();
+ div.classes.add('normal');
+ div.appendText(line);
+ $['consoleText'].children.add(div);
+ div.scrollIntoView();
+ }
+
+ void printBold(String line) {
+ var div = new DivElement();
+ div.classes.add('bold');
+ div.appendText(line);
+ $['consoleText'].children.add(div);
+ div.scrollIntoView();
+ }
+
+ void newline() {
+ var br = new BRElement();
+ $['consoleText'].children.add(br);
+ br.scrollIntoView();
+ }
}
@CustomTag('debugger-input')
class DebuggerInputElement extends ObservatoryElement {
@published Isolate isolate;
@published String text = '';
+ @observable Debugger debugger;
@override
void ready() {
@@ -118,13 +663,17 @@ class DebuggerInputElement extends ObservatoryElement {
switch (e.keyCode) {
case KeyCode.TAB:
e.preventDefault();
- textBox.setRangeText('TAB');
- textBox.setSelectionRange(textBox.selectionStart + 3,
- textBox.selectionStart + 3);
+ int cursorPos = textBox.selectionStart;
+ var completion = debugger.complete(text.substring(0, cursorPos));
+ text = completion + text.substring(cursorPos);
+ // TODO(turnidge): Move the cursor to the end of the
+ // completion, rather than the end of the string.
break;
case KeyCode.ENTER:
- print('Debugger command (not implemented): $text');
- text = '';
+ if (!debugger.busy) {
+ debugger.run(text);
+ text = '';
+ }
break;
}
});
« no previous file with comments | « runtime/observatory/lib/src/app/application.dart ('k') | runtime/observatory/lib/src/elements/debugger.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698