Index: runtime/observatory/lib/src/elements/memory/graph.dart |
diff --git a/runtime/observatory/lib/src/elements/memory/graph.dart b/runtime/observatory/lib/src/elements/memory/graph.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..298527cdb4a060812cd2b45d864bcd299a09b422 |
--- /dev/null |
+++ b/runtime/observatory/lib/src/elements/memory/graph.dart |
@@ -0,0 +1,429 @@ |
+// Copyright (c) 2017, 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. |
+ |
+/// This Element is part of MemoryDashboardElement. |
+/// |
+/// The Element periodically interrogates the VM to log the memory usage of each |
+/// Isolate and of the Native Memory. |
+/// |
+/// For each isolate it is shown the Used and Free heap (new and old are merged |
+/// together) |
+/// |
+/// When a GC event is received an extra point is introduced in the graph to |
+/// make the representation as precise as possible. |
+/// |
+/// When an Isolate is selected the event is bubbled up to the parent. |
+ |
+import 'dart:async'; |
+import 'dart:html'; |
+import 'package:observatory/models.dart' as M; |
+import 'package:charted/charted.dart'; |
+import 'package:observatory/src/elements/helpers/rendering_scheduler.dart'; |
+import 'package:observatory/src/elements/helpers/tag.dart'; |
+import 'package:observatory/utils.dart'; |
+ |
+class IsolateSelectedEvent { |
+ final M.Isolate isolate; |
+ |
+ const IsolateSelectedEvent([this.isolate]); |
+} |
+ |
+class MemoryGraphElement extends HtmlElement implements Renderable { |
+ static const tag = const Tag<MemoryGraphElement>('memory-graph'); |
+ |
+ RenderingScheduler<MemoryGraphElement> _r; |
+ |
+ final StreamController<IsolateSelectedEvent> _onIsolateSelected = |
+ new StreamController<IsolateSelectedEvent>(); |
+ |
+ Stream<RenderedEvent<MemoryGraphElement>> get onRendered => _r.onRendered; |
+ Stream<IsolateSelectedEvent> get onIsolateSelected => |
+ _onIsolateSelected.stream; |
+ |
+ M.VM _vm; |
+ M.IsolateRepository _isolates; |
+ M.EventRepository _events; |
+ StreamSubscription _onGCSubscription; |
+ StreamSubscription _onResizeSubscription; |
+ StreamSubscription _onConnectionClosedSubscription; |
+ Timer _onTimer; |
+ |
+ M.VM get vm => _vm; |
+ |
+ factory MemoryGraphElement( |
+ M.VM vm, M.IsolateRepository isolates, M.EventRepository events, |
+ {RenderingQueue queue}) { |
+ assert(vm != null); |
+ assert(isolates != null); |
+ assert(events != null); |
+ MemoryGraphElement e = document.createElement(tag.name); |
+ e._r = new RenderingScheduler(e, queue: queue); |
+ e._vm = vm; |
+ e._isolates = isolates; |
+ e._events = events; |
+ return e; |
+ } |
+ |
+ MemoryGraphElement.created() : super.created() { |
+ final now = new DateTime.now(); |
+ var sample = now.subtract(_window); |
+ while (sample.isBefore(now)) { |
+ _ts.add(sample); |
+ _vmSamples.add(0); |
+ _isolateUsedSamples.add([]); |
+ _isolateFreeSamples.add([]); |
+ sample = sample.add(_period); |
+ } |
+ _ts.add(now); |
+ _vmSamples.add(0); |
+ _isolateUsedSamples.add([]); |
+ _isolateFreeSamples.add([]); |
+ } |
+ |
+ static const Duration _period = const Duration(seconds: 2); |
+ static const Duration _window = const Duration(minutes: 2); |
+ |
+ @override |
+ attached() { |
+ super.attached(); |
+ _r.enable(); |
+ _onGCSubscription = |
+ _events.onGCEvent.listen((e) => _refresh(gcIsolate: e.isolate)); |
+ _onConnectionClosedSubscription = |
+ _events.onConnectionClosed.listen((_) => _onTimer.cancel()); |
+ _onResizeSubscription = window.onResize.listen((_) => _r.dirty()); |
+ _onTimer = new Timer.periodic(_period, (_) => _refresh()); |
+ _refresh(); |
+ } |
+ |
+ @override |
+ detached() { |
+ super.detached(); |
+ _r.disable(notify: true); |
+ children = []; |
+ _onGCSubscription.cancel(); |
+ _onConnectionClosedSubscription.cancel(); |
+ _onResizeSubscription.cancel(); |
+ _onTimer.cancel(); |
+ } |
+ |
+ final List<DateTime> _ts = <DateTime>[]; |
+ final List<int> _vmSamples = <int>[]; |
+ final List<M.IsolateRef> _seenIsolates = <M.IsolateRef>[]; |
+ final List<List<int>> _isolateUsedSamples = <List<int>>[]; |
+ final List<List<int>> _isolateFreeSamples = <List<int>>[]; |
+ final Map<String, int> _isolateIndex = <String, int>{}; |
+ final Map<String, String> _isolateName = <String, String>{}; |
+ |
+ var _selected; |
+ var _previewed; |
+ var _hovered; |
+ |
+ void render() { |
+ if (_previewed != null || _hovered != null) return; |
+ |
+ final now = _ts.last; |
+ final nativeComponents = 1; |
+ final legend = new DivElement(); |
+ final host = new DivElement(); |
+ final theme = new MemoryChartTheme(1); |
+ children = [theme.style, legend, host]; |
+ final rect = host.getBoundingClientRect(); |
+ |
+ final series = |
+ new List<int>.generate(_isolateIndex.length * 2 + 1, (i) => i + 1); |
+ // The stacked line chart sorts from top to bottom |
+ final columns = [ |
+ new ChartColumnSpec( |
+ formatter: _formatTimeAxis, type: ChartColumnSpec.TYPE_NUMBER), |
+ new ChartColumnSpec(label: 'Native', formatter: Utils.formatSize) |
+ ]..addAll(_isolateName.keys.expand((id) => [ |
+ new ChartColumnSpec(formatter: Utils.formatSize), |
+ new ChartColumnSpec(label: _label(id), formatter: Utils.formatSize) |
+ ])); |
+ // The stacked line chart sorts from top to bottom |
+ final rows = new List.generate(_ts.length, (sampleIndex) { |
+ final free = _isolateFreeSamples[sampleIndex]; |
+ final used = _isolateUsedSamples[sampleIndex]; |
+ return [ |
+ _ts[sampleIndex].difference(now).inMicroseconds, |
+ _vmSamples[sampleIndex] |
+ ]..addAll(_isolateIndex.keys.expand((key) { |
+ final isolateIndex = _isolateIndex[key]; |
+ return [free[isolateIndex], used[isolateIndex]]; |
+ })); |
+ }); |
+ |
+ final scale = new LinearScale()..domain = [(-_window).inMicroseconds, 0]; |
+ final axisConfig = new ChartAxisConfig()..scale = scale; |
+ final sMemory = |
+ new ChartSeries('Memory', series, new StackedLineChartRenderer()); |
+ final config = new ChartConfig([sMemory], [0]) |
+ ..legend = new ChartLegend(legend) |
+ ..registerDimensionAxis(0, axisConfig); |
+ config.minimumSize = new Rect(rect.width, rect.height); |
+ final data = new ChartData(columns, rows); |
+ final state = new ChartState(isMultiSelect: true) |
+ ..changes.listen(_handleEvent); |
+ final area = new CartesianArea(host, data, config, state: state) |
+ ..theme = theme; |
+ area.addChartBehavior(new Hovercard(builder: (int column, int row) { |
+ if (column == 1) { |
+ return _formatNativeOvercard(row); |
+ } |
+ return _formatIsolateOvercard(_seenIsolates[column - 2].id, row); |
+ })); |
+ area.draw(); |
+ |
+ if (_selected != null) { |
+ state.select(_selected); |
+ if (_selected > 1) { |
+ state.select(_selected + 1); |
+ } |
+ } |
+ } |
+ |
+ String _formatTimeAxis(num ms) => |
+ Utils.formatDuration(new Duration(microseconds: ms.toInt()), |
+ precision: DurationComponent.Seconds); |
+ |
+ bool _running = false; |
+ |
+ Future _refresh({M.IsolateRef gcIsolate}) async { |
+ if (_running) return; |
+ _running = true; |
+ final now = new DateTime.now(); |
+ final start = now.subtract(_window).subtract(_period); |
+ // The Service classes order isolates from the older to the newer |
+ final isolates = |
+ (await Future.wait(_vm.isolates.map(_isolates.get))).reversed.toList(); |
+ while (_ts.first.isBefore(start)) { |
+ _ts.removeAt(0); |
+ _vmSamples.removeAt(0); |
+ _isolateUsedSamples.removeAt(0); |
+ _isolateFreeSamples.removeAt(0); |
+ } |
+ |
+ if (_isolateIndex.length == 0) { |
+ _selected = isolates.length * 2; |
+ _onIsolateSelected.add(new IsolateSelectedEvent(isolates.last)); |
+ } |
+ |
+ isolates |
+ .where((isolate) => !_isolateIndex.containsKey(isolate.id)) |
+ .forEach((isolate) { |
+ _isolateIndex[isolate.id] = _isolateIndex.length; |
+ _seenIsolates.addAll([isolate, isolate]); |
+ }); |
+ |
+ if (_isolateIndex.length != _isolateName.length) { |
+ final extra = |
+ new List.filled(_isolateIndex.length - _isolateName.length, 0); |
+ _isolateUsedSamples.forEach((sample) => sample.addAll(extra)); |
+ _isolateFreeSamples.forEach((sample) => sample.addAll(extra)); |
+ } |
+ |
+ final length = _isolateIndex.length; |
+ |
+ if (gcIsolate != null) { |
+ // After GC we add an extra point to show the drop in a clear way |
+ final List<int> isolateUsedSample = new List<int>.filled(length, 0); |
+ final List<int> isolateFreeSample = new List<int>.filled(length, 0); |
+ isolates.forEach((M.Isolate isolate) { |
+ _isolateName[isolate.id] = isolate.name; |
+ final index = _isolateIndex[isolate.id]; |
+ if (isolate.id == gcIsolate) { |
+ isolateUsedSample[index] = |
+ _isolateUsedSamples.last[index] + _isolateFreeSamples.last[index]; |
+ isolateFreeSample[index] = 0; |
+ } else { |
+ isolateUsedSample[index] = _used(isolate); |
+ isolateFreeSample[index] = _free(isolate); |
+ } |
+ }); |
+ _isolateUsedSamples.add(isolateUsedSample); |
+ _isolateFreeSamples.add(isolateFreeSample); |
+ |
+ _vmSamples.add(vm.heapAllocatedMemoryUsage); |
+ |
+ _ts.add(now); |
+ } |
+ final List<int> isolateUsedSample = new List<int>.filled(length, 0); |
+ final List<int> isolateFreeSample = new List<int>.filled(length, 0); |
+ isolates.forEach((M.Isolate isolate) { |
+ _isolateName[isolate.id] = isolate.name; |
+ final index = _isolateIndex[isolate.id]; |
+ isolateUsedSample[index] = _used(isolate); |
+ isolateFreeSample[index] = _free(isolate); |
+ }); |
+ _isolateUsedSamples.add(isolateUsedSample); |
+ _isolateFreeSamples.add(isolateFreeSample); |
+ |
+ _vmSamples.add(vm.heapAllocatedMemoryUsage); |
+ |
+ _ts.add(now); |
+ _r.dirty(); |
+ _running = false; |
+ } |
+ |
+ void _handleEvent(records) => records.forEach((record) { |
+ if (record is ChartSelectionChangeRecord) { |
+ var selected = record.add; |
+ if (selected == null) { |
+ if (selected != _selected) { |
+ _onIsolateSelected.add(const IsolateSelectedEvent()); |
+ _r.dirty(); |
+ } |
+ } else { |
+ if (selected == 1) { |
+ if (selected != _selected) { |
+ _onIsolateSelected.add(const IsolateSelectedEvent()); |
+ _r.dirty(); |
+ } |
+ } else { |
+ selected -= selected % 2; |
+ if (selected != _selected) { |
+ _onIsolateSelected |
+ .add(new IsolateSelectedEvent(_seenIsolates[selected - 2])); |
+ _r.dirty(); |
+ } |
+ } |
+ } |
+ _selected = selected; |
+ _previewed = null; |
+ _hovered = null; |
+ } else if (record is ChartPreviewChangeRecord) { |
+ _previewed = record.previewed; |
+ } else if (record is ChartHoverChangeRecord) { |
+ _hovered = record.hovered; |
+ } |
+ }); |
+ |
+ int _used(M.Isolate i) => i.newSpace.used + i.oldSpace.used; |
+ int _capacity(M.Isolate i) => i.newSpace.capacity + i.oldSpace.capacity; |
+ int _free(M.Isolate i) => _capacity(i) - _used(i); |
+ |
+ String _label(String isolateId) { |
+ final index = _isolateIndex[isolateId]; |
+ final name = _isolateName[isolateId]; |
+ final free = _isolateFreeSamples.last[index]; |
+ final used = _isolateUsedSamples.last[index]; |
+ final usedStr = Utils.formatSize(used); |
+ final capacity = free + used; |
+ final capacityStr = Utils.formatSize(capacity); |
+ return '${name} ($usedStr / $capacityStr)'; |
+ } |
+ |
+ Element _formatNativeOvercard(int row) => new DivElement() |
+ ..children = [ |
+ new DivElement() |
+ ..classes = ['hovercard-title'] |
+ ..text = 'Native', |
+ new DivElement() |
+ ..classes = ['hovercard-measure', 'hovercard-multi'] |
+ ..children = [ |
+ new DivElement() |
+ ..classes = ['hovercard-measure-label'] |
+ ..text = 'Heap', |
+ new DivElement() |
+ ..classes = ['hovercard-measure-value'] |
+ ..text = Utils.formatSize(_vmSamples[row]), |
+ ] |
+ ]; |
+ |
+ Element _formatIsolateOvercard(String isolateId, int row) { |
+ final index = _isolateIndex[isolateId]; |
+ final free = _isolateFreeSamples[row][index]; |
+ final used = _isolateUsedSamples[row][index]; |
+ final capacity = free + used; |
+ return new DivElement() |
+ ..children = [ |
+ new DivElement() |
+ ..classes = ['hovercard-title'] |
+ ..text = _isolateName[isolateId], |
+ new DivElement() |
+ ..classes = ['hovercard-measure', 'hovercard-multi'] |
+ ..children = [ |
+ new DivElement() |
+ ..classes = ['hovercard-measure-label'] |
+ ..text = 'Heap Capacity', |
+ new DivElement() |
+ ..classes = ['hovercard-measure-value'] |
+ ..text = Utils.formatSize(capacity), |
+ ], |
+ new DivElement() |
+ ..classes = ['hovercard-measure', 'hovercard-multi'] |
+ ..children = [ |
+ new DivElement() |
+ ..classes = ['hovercard-measure-label'] |
+ ..text = 'Free Heap', |
+ new DivElement() |
+ ..classes = ['hovercard-measure-value'] |
+ ..text = Utils.formatSize(free), |
+ ], |
+ new DivElement() |
+ ..classes = ['hovercard-measure', 'hovercard-multi'] |
+ ..children = [ |
+ new DivElement() |
+ ..classes = ['hovercard-measure-label'] |
+ ..text = 'Used Heap', |
+ new DivElement() |
+ ..classes = ['hovercard-measure-value'] |
+ ..text = Utils.formatSize(used), |
+ ] |
+ ]; |
+ } |
+} |
+ |
+class MemoryChartTheme extends QuantumChartTheme { |
+ final int _offset; |
+ |
+ MemoryChartTheme(int offset) : _offset = offset { |
+ assert(offset != null); |
+ assert(offset >= 0); |
+ } |
+ |
+ @override |
+ String getColorForKey(key, [int state = 0]) { |
+ key -= 1; |
+ if (key > _offset) { |
+ key = _offset + (key - _offset) ~/ 2; |
+ } |
+ key += 1; |
+ return super.getColorForKey(key, state); |
+ } |
+ |
+ @override |
+ String getFilterForState(int state) => state & ChartState.COL_PREVIEW != 0 || |
+ state & ChartState.VAL_HOVERED != 0 || |
+ state & ChartState.COL_SELECTED != 0 || |
+ state & ChartState.VAL_HIGHLIGHTED != 0 |
+ ? 'url(#drop-shadow)' |
+ : ''; |
+ |
+ @override |
+ String get filters => |
+ '<defs>' + |
+ super.filters + |
+ ''' |
+<filter id="stroke-grid" primitiveUnits="userSpaceOnUse"> |
+ <feFlood in="SourceGraphic" x="0" y="0" width="4" height="4" |
+ flood-color="black" flood-opacity="0.2" result='Black'/> |
+ <feFlood in="SourceGraphic" x="1" y="1" width="3" height="3" |
+ flood-color="black" flood-opacity="0.8" result='White'/> |
+ <feComposite in="Black" in2="White" operator="xor" x="0" y="0" width="4" height="4"/> |
+ <feTile x="0" y="0" width="100%" height="100%" /> |
+ <feComposite in2="SourceAlpha" result="Pattern" operator="in" x="0" y="0" width="100%" height="100%"/> |
+ <feComposite in="SourceGraphic" in2="Pattern" operator="atop" x="0" y="0" width="100%" height="100%"/> |
+</filter> |
+ </defs> |
+'''; |
+ |
+ StyleElement get style => new StyleElement() |
+ ..text = ''' |
+memory-graph svg .stacked-line-rdr-line:nth-child(2n+${_offset+1}) |
+ path:nth-child(1) { |
+ filter: url(#stroke-grid); |
+}'''; |
+} |