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

Unified Diff: runtime/observatory/lib/src/elements/memory/graph.dart

Issue 2989083002: Add memory-dashboard page to Observatory (Closed)
Patch Set: Addressed CL comments Created 3 years, 4 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/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);
+}''';
+}
« no previous file with comments | « runtime/observatory/lib/src/elements/memory/dashboard.dart ('k') | runtime/observatory/lib/src/elements/memory/profile.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698