Chromium Code Reviews| Index: runtime/bin/vmservice/client/lib/src/service/object.dart |
| diff --git a/runtime/bin/vmservice/client/lib/src/service/object.dart b/runtime/bin/vmservice/client/lib/src/service/object.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f92a20d13c42bb05ee2c1cb80d787beb76b59bbf |
| --- /dev/null |
| +++ b/runtime/bin/vmservice/client/lib/src/service/object.dart |
| @@ -0,0 +1,621 @@ |
| +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +part of service; |
| + |
| +/// State for a running isolate. |
| +class Isolate extends ServiceObject { |
| + final VM vm; |
| + String get link => _id; |
| + String get hashLink => '#/$_id'; |
| + |
| + ScriptCache _scripts; |
|
turnidge
2014/03/10 21:03:28
consider blank lines between decls.
|
| + /// Script cache. |
| + ScriptCache get scripts => _scripts; |
| + CodeCache _codes; |
| + /// Code cache. |
| + CodeCache get codes => _codes; |
| + /// Class cache. |
| + ClassCache _classes; |
| + ClassCache get classes => _classes; |
| + |
| + void _initOnce() { |
| + // Only called once. |
| + assert(_isolate == null); |
| + _isolate = this; |
| + _scripts = new ScriptCache(this); |
| + _codes = new CodeCache(this); |
| + _classes = new ClassCache(this); |
| + } |
| + |
| + Isolate.fromId(this.vm, String id) : super(null, id, '@Isolate') { |
| + _initOnce(); |
| + } |
| + |
| + Isolate.fromMap(this.vm, Map map) : super.fromMap(null, map) { |
| + _initOnce(); |
| + } |
| + |
| + /// Creates a link to [id] relative to [this]. |
| + @reflectable String relativeLink(String id) => '${this.id}/$id'; |
| + /// Creates a relative link to [id] with a '#/' prefix. |
| + @reflectable String relativeHashLink(String id) => '#/${relativeLink(id)}'; |
| + |
| + Future<ScriptCache> refreshCoverage() { |
| + return fetch('coverage').then(_scripts._processCoverage); |
| + } |
| + |
| + void processProfile(ServiceMap profile) { |
| + assert(profile.serviceType == 'Profile'); |
| + var codeTable = new List<Code>(); |
| + var profileCodes = profile['codes']; |
| + for (var profileCode in profileCodes) { |
| + Code code = profileCode['code']; |
| + codeTable.add(code); |
| + } |
| + _codes._resetProfileData(); |
| + _codes._updateProfileData(profile, codeTable); |
| + } |
| + |
| + /// Requests [serviceId] from [this]. Completes to a [ServiceObject]. |
| + /// Can return pre-existing, cached, [ServiceObject]s. |
| + Future<ServiceObject> get(String serviceId) { |
| + if (_scripts.cachesId(serviceId)) { |
| + return _scripts.get(serviceId); |
| + } |
| + if (_codes.cachesId(serviceId)) { |
| + return _codes.get(serviceId); |
| + } |
| + if (_classes.cachesId(serviceId)) { |
| + return _classes.get(serviceId); |
| + } |
| + return vm.get(this, relativeLink(serviceId)); |
| + } |
| + |
| + /// Fetches [serviceId] from [this]. Completes to a [ServiceObject]. |
| + /// Always creates a new ServiceObject. |
| + Future<ServiceObject> fetch(String serviceId, [Map options]) { |
|
turnidge
2014/03/10 21:03:28
I find the distinction between the names "get" and
|
| + return vm.get(this, relativeLink(serviceId)); |
| + } |
| + |
| + |
|
turnidge
2014/03/10 21:03:28
Remove one blank line here?
|
| + @observable ServiceMap rootLib; |
| + @observable ServiceMap topFrame; |
| + |
| + @observable String name; |
| + @observable String vmName; |
| + @observable Map entry; |
| + |
| + @observable final Map<String, double> timers = |
| + toObservable(new Map<String, double>()); |
| + |
| + @observable int newHeapUsed = 0; |
| + @observable int oldHeapUsed = 0; |
| + |
| + @observable String fileAndLine; |
| + |
| + void _update(ObservableMap map) { |
| + if (map['type'] == '@Isolate') { |
| + if (isRef()) { |
| + // Eagerly derefence isolates. |
| + deref(); |
| + } |
| + return; |
| + } |
| + _ref = false; |
| + if (map['rootLib'] == null || |
| + map['timers'] == null || |
| + map['heap'] == null) { |
| + Logger.root.severe("Malformed 'Isolate' response: $map"); |
| + return; |
| + } |
| + rootLib = new ServiceMap.fromMap(this, map['rootLib']); |
| + vmName = map['name']; |
| + if (map['entry'] != null) { |
| + entry = map['entry']; |
| + name = entry['name']; |
| + } else { |
| + // fred |
| + name = 'root isolate'; |
| + } |
| + if (map['topFrame'] != null) { |
| + topFrame = new ServiceMap.fromMap(this, map['topFrame']); |
| + } else { |
| + topFrame = null ; |
| + } |
| + |
| + var timerMap = {}; |
| + map['timers'].forEach((timer) { |
| + timerMap[timer['name']] = timer['time']; |
| + }); |
| + timers['total'] = timerMap['time_total_runtime']; |
| + timers['compile'] = timerMap['time_compilation']; |
| + timers['gc'] = 0.0; // TODO(turnidge): Export this from VM. |
| + timers['init'] = (timerMap['time_script_loading'] + |
| + timerMap['time_creating_snapshot'] + |
| + timerMap['time_isolate_initialization'] + |
| + timerMap['time_bootstrap']); |
| + timers['dart'] = timerMap['time_dart_execution']; |
| + |
| + newHeapUsed = map['heap']['usedNew']; |
| + oldHeapUsed = map['heap']['usedOld']; |
| + } |
| +} |
| + |
| +// TODO(johnmccutchan): Make this into an IsolateCache. |
| +class IsolateList extends ServiceObject { |
| + final VM _vm; |
| + VM get vm => _vm; |
| + @observable final ObservableMap isolates = new ObservableMap(); |
| + IsolateList(this._vm) : super(null, 'isolates', 'IsolateList') { |
| + name = 'IsolateList'; |
| + vmName = name; |
| + } |
| + IsolateList.fromMap(this._vm, Map m) : super.fromMap(null, m) { |
| + name = 'IsolateList'; |
| + vmName = name; |
| + } |
| + |
| + Future<ServiceObject> refresh() { |
| + return vm.fetchMap(id).then(update); |
| + } |
| + |
| + void _update(ObservableMap map) { |
| + _updateIsolates(map['members']); |
| + } |
| + |
| + void _updateIsolates(List<Map> members) { |
| + // Find dead isolates. |
| + var deadIsolates = []; |
| + isolates.forEach((k, v) { |
| + if (!_foundIsolateInMembers(k, members)) { |
| + deadIsolates.add(k); |
| + } |
| + }); |
| + // Remove them. |
| + deadIsolates.forEach((id) { |
| + isolates.remove(id); |
| + Logger.root.info('Isolate \'$id\' has gone away.'); |
| + }); |
| + |
| + // Add new isolates. |
| + members.forEach((map) { |
| + var id = map['id']; |
| + var isolate = isolates[id]; |
| + if (isolate == null) { |
| + isolate = new Isolate.fromMap(vm, map); |
| + Logger.root.info('Created ServiceObject for \'${isolate.id}\' with ' |
| + 'type \'${isolate.serviceType}\''); |
| + isolates[id] = isolate; |
| + } |
| + }); |
| + |
| + // After updating the isolate list, refresh each isolate. |
| + _refreshIsolates(); |
| + } |
| + |
| + void _refreshIsolates() { |
| + // This is technically asynchronous but we don't need to wait for |
| + // the result. |
| + isolates.forEach((k, Isolate isolate) { |
| + isolate.refresh(); |
| + }); |
| + } |
| + |
| + Isolate getIsolate(String id) { |
| + assert(id.startsWith('isolates/')); |
| + var isolate = isolates[id]; |
| + if (isolate != null) { |
| + return isolate; |
| + } |
| + isolate = new Isolate.fromId(vm, id); |
| + isolates[id] = isolate; |
| + isolate.deref(); |
| + return isolate; |
| + } |
| + |
| + static bool _foundIsolateInMembers(String id, List<Map> members) { |
| + return members.any((E) => E['id'] == id); |
| + } |
| +} |
| + |
| + |
| +/// A [ServiceObject] which implements [ObservableMap]. |
| +class ServiceMap extends ServiceObject implements ObservableMap { |
| + final ObservableMap _map = new ObservableMap(); |
| + ServiceMap(Isolate isolate, String id, String serviceType) : |
| + super(isolate, id, serviceType) { |
| + } |
| + |
| + ServiceMap.fromMap(Isolate isolate, ObservableMap m) : |
| + super.fromMap(isolate, m); |
| + |
| + bool _isUpgradeTarget(v) { |
| + return (v is Map) && ServiceObject.isServiceMap(v); |
| + } |
| + |
| + String toString() => _map.toString(); |
| + |
| + ServiceObject _upgrade(Map m) { |
| + var upgraded = _upgradeToServiceObject(isolate.vm, isolate, m); |
| + if (upgraded is ServiceMap) { |
| + // Recurse. |
| + upgraded._upgradeValues(); |
| + } |
| + return upgraded; |
| + } |
| + |
| + void _upgradeList(List l) { |
| + for (var i = 0; i < l.length; i++) { |
| + if (_isUpgradeTarget(l[i])) { |
| + l[i] = _upgrade(l[i]); |
| + } else if (l[i] is List) { |
| + _upgradeList(l[i]); |
| + } else if (l[i] is Map) { |
| + _upgradeMap(l[i]); |
| + } |
| + } |
| + } |
| + |
| + void _upgradeMap(Map m) { |
| + m.forEach((k, v) { |
| + if (_isUpgradeTarget(v)) { |
| + m[k] = _upgrade(v); |
| + } else if (v is List) { |
| + _upgradeList(v); |
| + } else if (v is Map) { |
| + _upgradeMap(v); |
| + } |
| + }); |
| + } |
| + |
| + void _upgradeValues() { |
| + assert(isolate != null); |
| + _upgradeMap(_map); |
| + } |
| + |
| + void _update(ObservableMap m) { |
| + _map.clear(); |
| + _map.addAll(m); |
| + name = _map['user_name']; |
| + vmName = _map['name']; |
| + _upgradeValues(); |
| + } |
| + |
| + // Forward Map interface calls. |
| + void addAll(Map other) => _map.addAll(other); |
| + void clear() => _map.clear(); |
| + bool containsValue(v) => _map.containsValue(v); |
| + bool containsKey(k) => _map.containsKey(k); |
| + void forEach(Function f) => _map.forEach(f); |
| + putIfAbsent(key, Function ifAbsent) => _map.putIfAbsent(key, ifAbsent); |
| + void remove(key) => _map.remove(key); |
| + operator [](k) => _map[k]; |
| + operator []=(k, v) => _map[k] = v; |
| + bool get isEmpty => _map.isEmpty; |
| + bool get isNotEmpty => _map.isNotEmpty; |
| + Iterable get keys => _map.keys; |
| + Iterable get values => _map.values; |
| + int get length => _map.length; |
| + |
| + // Forward ChangeNotifier interface calls. |
| + bool deliverChanges() => _map.deliverChanges(); |
| + void notifyChange(ChangeRecord record) => _map.notifyChange(record); |
| + notifyPropertyChange(Symbol field, Object oldValue, Object newValue) => |
| + _map.notifyPropertyChange(field, oldValue, newValue); |
| + void observed() => _map.observed(); |
| + void unobserved() => _map.unobserved(); |
| + Stream<List<ChangeRecord>> get changes => _map.changes; |
| + bool get hasObservers => _map.hasObservers; |
| +} |
| + |
| +class ServiceError extends ServiceObject { |
| + ServiceError.fromMap(Isolate isolate, Map m) : super.fromMap(isolate, m); |
| + |
| + @observable String kind; |
| + @observable String message; |
| + |
| + void _update(ObservableMap map) { |
| + kind = map['kind']; |
| + message = map['message']; |
| + name = 'ServiceError $kind'; |
| + vmName = name; |
| + } |
| + |
| + // TODO: stackTrace? |
| +} |
| + |
| +class ScriptLine { |
| + @reflectable final int line; |
| + @reflectable final String text; |
| + ScriptLine(this.line, this.text); |
| +} |
| + |
| +class Script extends ServiceObject { |
| + @reflectable final lines = new ObservableList<ScriptLine>(); |
| + @reflectable final hits = new ObservableMap<int, int>(); |
| + @observable ServiceObject library; |
| + @observable String kind; |
| + |
| + String _shortUrl; |
| + String _url; |
| + |
| + Script.fromMap(Isolate isolate, Map m) : super.fromMap(isolate, m); |
| + |
| + void _update(ObservableMap m) { |
| + // Assert that m is a service map. |
| + assert(ServiceObject.isServiceMap(m)); |
| + if ((m['type'] == 'Error') && (m['kind'] == 'NotFoundError')) { |
| + // TODO(johnmccutchan): Find out why dart:core/identical.dart can't |
| + // be found but shows up in coverage. i.e. a function has reference |
| + // to script that no library does. |
| + Logger.root.info(m['message']); |
| + return; |
| + } |
| + // Assert that the id hasn't changed. |
| + assert(m['id'] == _id); |
| + // Assert that the type hasn't changed. |
| + assert(ServiceObject.unreffedType(m['type']) == _serviceType); |
| + _url = m['name']; |
| + _shortUrl = _url.substring(_url.lastIndexOf('/') + 1); |
| + name = _shortUrl; |
| + vmName = _url; |
| + kind = m['kind']; |
| + _processSource(m['source']); |
| + } |
| + |
| + void _processHits(List scriptHits) { |
| + if (_ref) { |
| + // Eagerly grab script source. |
| + deref(); |
| + } |
| + // Update hits table. |
| + for (var i = 0; i < scriptHits.length; i += 2) { |
| + var line = scriptHits[i]; |
| + var hit = scriptHits[i + 1]; // hit status. |
| + assert(line >= 1); // Lines start at 1. |
| + hits[line] = hit; |
| + } |
| + } |
| + |
| + void _processSource(String source) { |
| + // Preemptyively mark that this is a reference. |
|
turnidge
2014/03/10 21:03:28
typo in comment.
|
| + _ref = true; |
| + if (source == null) { |
| + return; |
| + } |
| + var sourceLines = source.split('\n'); |
| + if (sourceLines.length == 0) { |
| + return; |
| + } |
| + // We have the source to the script. This is no longer a reference. |
| + _ref = false; |
| + lines.clear(); |
| + Logger.root.info('Adding ${sourceLines.length} source lines for ${_url}'); |
| + for (var i = 0; i < sourceLines.length; i++) { |
| + lines.add(new ScriptLine(i + 1, sourceLines[i])); |
| + } |
| + } |
| + |
| + |
| +} |
| + |
| +class CodeTick { |
| + final int address; |
| + final int exclusiveTicks; |
| + final int inclusiveTicks; |
| + CodeTick(this.address, this.exclusiveTicks, this.inclusiveTicks); |
| +} |
| + |
| + |
| +class CodeInstruction extends Observable { |
| + @observable final int address; |
| + @observable final String machine; |
| + @observable final String human; |
| + |
| + static String formatPercent(num a, num total) { |
| + var percent = 100.0 * (a / total); |
| + return '${percent.toStringAsFixed(2)}%'; |
| + } |
| + |
| + CodeInstruction(this.address, this.machine, this.human); |
| + |
| + @reflectable String formattedAddress() { |
| + if (address == 0) { |
| + return ''; |
| + } |
| + return '0x${address.toRadixString(16)}'; |
| + } |
| + |
| + @reflectable String formattedInclusive(Code code) { |
| + if (code == null) { |
| + return ''; |
| + } |
| + var tick = code.addressTicks[address]; |
| + if (tick == null) { |
| + return ''; |
| + } |
| + var pcent = formatPercent(tick.inclusiveTicks, code.totalSamplesInProfile); |
| + return '${tick.inclusiveTicks} ($pcent)'; |
| + } |
| + |
| + @reflectable String formattedExclusive(Code code) { |
| + if (code == null) { |
| + return ''; |
| + } |
| + var tick = code.addressTicks[address]; |
| + if (tick == null) { |
| + return ''; |
| + } |
| + var pcent = formatPercent(tick.exclusiveTicks, code.totalSamplesInProfile); |
| + return '${tick.exclusiveTicks} ($pcent)'; |
| + } |
| +} |
| + |
| +class CodeKind { |
| + final _value; |
| + const CodeKind._internal(this._value); |
| + String toString() => 'CodeKind.$_value'; |
| + |
| + static CodeKind fromString(String s) { |
| + if (s == 'Native') { |
| + return Native; |
| + } else if (s == 'Dart') { |
| + return Dart; |
| + } else if (s == 'Collected') { |
| + return Collected; |
| + } |
| + throw new FallThroughError(); |
| + } |
| + static const Native = const CodeKind._internal('Native'); |
| + static const Dart = const CodeKind._internal('Dart'); |
| + static const Collected = const CodeKind._internal('Collected'); |
| +} |
| + |
| +class CodeCallCount { |
| + final Code code; |
| + final int count; |
| + CodeCallCount(this.code, this.count); |
| +} |
| + |
| +class Code extends ServiceObject { |
| + @observable CodeKind kind; |
| + @observable int totalSamplesInProfile = 0; |
| + @reflectable int exclusiveTicks = 0; |
| + @reflectable int inclusiveTicks = 0; |
| + @reflectable int startAddress = 0; |
| + @reflectable int endAddress = 0; |
| + @reflectable final callers = new List<CodeCallCount>(); |
| + @reflectable final callees = new List<CodeCallCount>(); |
| + @reflectable final instructions = new ObservableList<CodeInstruction>(); |
| + @reflectable final addressTicks = new ObservableMap<int, CodeTick>(); |
| + |
| + @observable ServiceMap function; |
| + String name; |
| + String vmName; |
| + |
| + Code.fromMap(Isolate isolate, Map map) : super.fromMap(isolate, map); |
| + |
| + // Reset all data associated with a profile. |
| + void resetProfileData() { |
| + totalSamplesInProfile = 0; |
| + exclusiveTicks = 0; |
| + inclusiveTicks = 0; |
| + callers.clear(); |
| + callees.clear(); |
| + addressTicks.clear(); |
| + } |
| + |
| + void _resolveCalls(List<CodeCallCount> calls, List data, List<Code> codes) { |
| + // Assert that this has been cleared. |
| + assert(calls.length == 0); |
| + // Resolve. |
| + for (var i = 0; i < data.length; i += 2) { |
| + var index = int.parse(data[i]); |
| + var count = int.parse(data[i + 1]); |
| + assert(index >= 0); |
| + assert(index < codes.length); |
| + calls.add(new CodeCallCount(codes[index], count)); |
| + } |
| + // Sort to descending count order. |
| + calls.sort((a, b) => b.count - a.count); |
| + } |
| + |
| + |
| + void updateProfileData(Map profileData, |
| + List<Code> codeTable, |
| + int sampleCount) { |
| + // Assert we have a ProfileCode entry. |
| + assert(profileData['type'] == 'ProfileCode'); |
| + // Assert we are handed profile data for this code object. |
| + assert(profileData['code'] == this); |
| + totalSamplesInProfile = sampleCount; |
| + inclusiveTicks = int.parse(profileData['inclusive_ticks']); |
| + exclusiveTicks = int.parse(profileData['exclusive_ticks']); |
| + _resolveCalls(callers, profileData['callers'], codeTable); |
| + _resolveCalls(callees, profileData['callees'], codeTable); |
| + var ticks = profileData['ticks']; |
| + if (ticks != null) { |
| + _processTicks(ticks); |
| + } |
| + } |
| + |
| + void _update(ObservableMap m) { |
| + assert(ServiceObject.isServiceMap(m)); |
| + assert(m['id'] == _id); |
| + assert(ServiceObject.unreffedType(m['type']) == _serviceType); |
| + name = m['user_name']; |
| + vmName = m['name']; |
| + startAddress = int.parse(m['start'], radix:16); |
| + endAddress = int.parse(m['end'], radix:16); |
| + // Upgrade the function. |
| + function = _upgradeToServiceObject(isolate.vm, isolate, m['function']); |
| + var disassembly = m['disassembly']; |
| + if (disassembly != null) { |
| + _processDisassembly(disassembly); |
| + } |
| + // We are a reference if we don't have instructions. |
| + _ref = (instructions.length == 0); |
| + } |
| + |
| + void _processDisassembly(List<String> disassembly){ |
| + assert(disassembly != null); |
| + instructions.clear(); |
| + assert((disassembly.length % 3) == 0); |
| + for (var i = 0; i < disassembly.length; i += 3) { |
| + var address = 0; // Assume code comment. |
| + var machine = disassembly[i + 1]; |
| + var human = disassembly[i + 2]; |
| + if (disassembly[i] != '') { |
| + // Not a code comment, extract address. |
| + address = int.parse(disassembly[i]); |
| + } |
| + var instruction = new CodeInstruction(address, machine, human); |
| + instructions.add(instruction); |
| + } |
| + } |
| + |
| + void _processTicks(List<String> profileTicks) { |
| + assert(profileTicks != null); |
| + assert((profileTicks.length % 3) == 0); |
| + for (var i = 0; i < profileTicks.length; i += 3) { |
| + var address = int.parse(profileTicks[i], radix:16); |
| + var exclusive = int.parse(profileTicks[i + 1]); |
| + var inclusive = int.parse(profileTicks[i + 2]); |
| + var tick = new CodeTick(address, exclusive, inclusive); |
| + addressTicks[address] = tick; |
| + } |
| + } |
| + |
| + /// Returns true if [address] is contained inside [this]. |
| + bool contains(int address) { |
| + return (address >= startAddress) && (address < endAddress); |
| + } |
| + |
| + /// Sum all caller counts. |
| + int sumCallersCount() => _sumCallCount(callers); |
| + /// Specific caller count. |
| + int callersCount(Code code) => _callCount(callers, code); |
| + /// Sum of callees count. |
| + int sumCalleesCount() => _sumCallCount(callees); |
| + /// Specific callee count. |
| + int calleesCount(Code code) => _callCount(callees, code); |
| + |
| + int _sumCallCount(List<CodeCallCount> calls) { |
| + var sum = 0; |
| + for (CodeCallCount caller in calls) { |
| + sum += caller.count; |
| + } |
| + return sum; |
| + } |
| + |
| + int _callCount(List<CodeCallCount> calls, Code code) { |
| + for (CodeCallCount caller in calls) { |
| + if (caller.code == code) { |
| + return caller.count; |
| + } |
| + } |
| + return 0; |
| + } |
| +} |