OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 import 'dart:async'; |
| 6 import 'dart:html'; |
| 7 import 'package:observatory/models.dart' as M; |
| 8 import 'package:charted/charted.dart'; |
| 9 import 'package:observatory/src/elements/helpers/rendering_scheduler.dart'; |
| 10 import 'package:observatory/src/elements/helpers/tag.dart'; |
| 11 import 'package:observatory/utils.dart'; |
| 12 |
| 13 class IsolateSelectedEvent { |
| 14 final M.Isolate isolate; |
| 15 |
| 16 const IsolateSelectedEvent([this.isolate]); |
| 17 } |
| 18 |
| 19 class DataPoint {} |
| 20 |
| 21 class MemoryGraphElement extends HtmlElement implements Renderable { |
| 22 static const tag = const Tag<MemoryGraphElement>('memory-graph'); |
| 23 |
| 24 RenderingScheduler<MemoryGraphElement> _r; |
| 25 |
| 26 final StreamController<IsolateSelectedEvent> _onIsolateSelected = |
| 27 new StreamController<IsolateSelectedEvent>(); |
| 28 |
| 29 Stream<RenderedEvent<MemoryGraphElement>> get onRendered => _r.onRendered; |
| 30 Stream<IsolateSelectedEvent> get onIsolateSelected => |
| 31 _onIsolateSelected.stream; |
| 32 |
| 33 M.VM _vm; |
| 34 M.IsolateRepository _isolates; |
| 35 M.EventRepository _events; |
| 36 StreamSubscription _onGCSubscription; |
| 37 Timer _onTimer; |
| 38 |
| 39 M.VM get vm => _vm; |
| 40 |
| 41 factory MemoryGraphElement( |
| 42 M.VM vm, M.IsolateRepository isolates, M.EventRepository events, |
| 43 {RenderingQueue queue}) { |
| 44 assert(vm != null); |
| 45 assert(isolates != null); |
| 46 assert(events != null); |
| 47 MemoryGraphElement e = document.createElement(tag.name); |
| 48 e._r = new RenderingScheduler(e, queue: queue); |
| 49 e._vm = vm; |
| 50 e._isolates = isolates; |
| 51 e._events = events; |
| 52 return e; |
| 53 } |
| 54 |
| 55 MemoryGraphElement.created() : super.created() { |
| 56 final now = new DateTime.now(); |
| 57 var sample = now.subtract(_window); |
| 58 while (sample.isBefore(now)) { |
| 59 _ts.add(sample); |
| 60 _vmSamples.add(0); |
| 61 _isolateUsedSamples.add([]); |
| 62 _isolateFreeSamples.add([]); |
| 63 sample = sample.add(_period); |
| 64 } |
| 65 _ts.add(now); |
| 66 _vmSamples.add(0); |
| 67 _isolateUsedSamples.add([]); |
| 68 _isolateFreeSamples.add([]); |
| 69 } |
| 70 |
| 71 static const Duration _period = const Duration(seconds: 2); |
| 72 static const Duration _window = const Duration(minutes: 2); |
| 73 |
| 74 @override |
| 75 attached() { |
| 76 super.attached(); |
| 77 _r.enable(); |
| 78 _onGCSubscription = |
| 79 _events.onGCEvent.listen((e) => _refresh(gcIsolate: e.isolate)); |
| 80 _onTimer = new Timer.periodic(_period, (_) => _refresh()); |
| 81 _refresh(); |
| 82 } |
| 83 |
| 84 @override |
| 85 detached() { |
| 86 super.detached(); |
| 87 _r.disable(notify: true); |
| 88 children = []; |
| 89 _onGCSubscription.cancel(); |
| 90 _onTimer.cancel(); |
| 91 } |
| 92 |
| 93 final List<DateTime> _ts = <DateTime>[]; |
| 94 final List<int> _vmSamples = <int>[]; |
| 95 final List<M.IsolateRef> _seenIsolates = <M.IsolateRef>[]; |
| 96 final List<List<int>> _isolateUsedSamples = <List<int>>[]; |
| 97 final List<List<int>> _isolateFreeSamples = <List<int>>[]; |
| 98 final Map<String, int> _isolateIndex = <String, int>{}; |
| 99 final Map<String, String> _isolateName = <String, String>{}; |
| 100 |
| 101 var _selected; |
| 102 var _previewed; |
| 103 var _hovered; |
| 104 |
| 105 void render() { |
| 106 if (_previewed != null || _hovered != null) return; |
| 107 |
| 108 final nativeComponents = 1; |
| 109 final legend = new DivElement(); |
| 110 final host = new DivElement(); |
| 111 final theme = new MemoryChartTheme(1); |
| 112 children = [theme.style, legend, host]; |
| 113 final rect = host.getBoundingClientRect(); |
| 114 |
| 115 final series = |
| 116 new List<int>.generate(_isolateIndex.length * 2 + 1, (i) => i + 1); |
| 117 // The stacked line chart sorts from top to bottom |
| 118 final columns = [ |
| 119 new ChartColumnSpec( |
| 120 formatter: Utils.formatDateTime, |
| 121 type: ChartColumnSpec.TYPE_TIMESTAMP), |
| 122 new ChartColumnSpec(label: 'Native', formatter: Utils.formatSize) |
| 123 ]..addAll(_isolateName.keys.expand((id) => [ |
| 124 new ChartColumnSpec(formatter: Utils.formatSize), |
| 125 new ChartColumnSpec(label: _label(id), formatter: Utils.formatSize) |
| 126 ])); |
| 127 // The stacked line chart sorts from top to bottom |
| 128 final rows = new List.generate(_ts.length, (sampleIndex) { |
| 129 final free = _isolateFreeSamples[sampleIndex]; |
| 130 final used = _isolateUsedSamples[sampleIndex]; |
| 131 return [_ts[sampleIndex], _vmSamples[sampleIndex]] |
| 132 ..addAll(_isolateIndex.keys.expand((key) { |
| 133 final isolateIndex = _isolateIndex[key]; |
| 134 return [free[isolateIndex], used[isolateIndex]]; |
| 135 })); |
| 136 }); |
| 137 |
| 138 final sMemory = |
| 139 new ChartSeries('Memory', series, new StackedLineChartRenderer()); |
| 140 final config = new ChartConfig([sMemory], [0]) |
| 141 ..legend = new ChartLegend(legend); |
| 142 config.minimumSize = new Rect(rect.width, rect.height); |
| 143 final data = new ChartData(columns, rows); |
| 144 final state = new ChartState(isMultiSelect: true) |
| 145 ..changes.listen(_handleEvent); |
| 146 final area = new CartesianArea(host, data, config, state: state) |
| 147 ..theme = theme; |
| 148 area.addChartBehavior(new Hovercard(builder: (int column, int row) { |
| 149 if (column == 1) { |
| 150 return _formatNativeOvercard(row); |
| 151 } |
| 152 return _formatIsolateOvercard(_seenIsolates[column - 2].id, row); |
| 153 })); |
| 154 area.draw(); |
| 155 |
| 156 if (_selected != null) { |
| 157 state.select(_selected); |
| 158 if (_selected > 1) { |
| 159 state.select(_selected + 1); |
| 160 } |
| 161 } |
| 162 ; |
| 163 } |
| 164 |
| 165 bool _running = false; |
| 166 |
| 167 Future _refresh({M.IsolateRef gcIsolate}) async { |
| 168 if (_running) return; |
| 169 _running = true; |
| 170 final now = new DateTime.now(); |
| 171 final start = now.subtract(_window); |
| 172 // The Service classes order isolates from the older to the newer |
| 173 final isolates = |
| 174 (await Future.wait(_vm.isolates.map(_isolates.get))).reversed.toList(); |
| 175 while (_ts.first.isBefore(start)) { |
| 176 _ts.removeAt(0); |
| 177 _vmSamples.removeAt(0); |
| 178 _isolateUsedSamples.removeAt(0); |
| 179 _isolateFreeSamples.removeAt(0); |
| 180 } |
| 181 |
| 182 if (_isolateIndex.length == 0) { |
| 183 _selected = isolates.length * 2; |
| 184 _onIsolateSelected.add(new IsolateSelectedEvent(isolates.last)); |
| 185 } |
| 186 |
| 187 isolates |
| 188 .where((isolate) => !_isolateIndex.containsKey(isolate.id)) |
| 189 .forEach((isolate) { |
| 190 _isolateIndex[isolate.id] = _isolateIndex.length; |
| 191 _seenIsolates.addAll([isolate, isolate]); |
| 192 }); |
| 193 |
| 194 if (_isolateIndex.length != _isolateName.length) { |
| 195 final extra = |
| 196 new List.filled(_isolateIndex.length - _isolateName.length, 0); |
| 197 _isolateUsedSamples.forEach((sample) => sample.addAll(extra)); |
| 198 _isolateFreeSamples.forEach((sample) => sample.addAll(extra)); |
| 199 } |
| 200 |
| 201 final length = _isolateIndex.length; |
| 202 |
| 203 if (gcIsolate != null) { |
| 204 // After GC we add an extra point to show the drop in a clear way |
| 205 final List<int> isolateUsedSample = new List<int>.filled(length, 0); |
| 206 final List<int> isolateFreeSample = new List<int>.filled(length, 0); |
| 207 isolates.forEach((M.Isolate isolate) { |
| 208 _isolateName[isolate.id] = isolate.name; |
| 209 final index = _isolateIndex[isolate.id]; |
| 210 if (isolate.id == gcIsolate) { |
| 211 isolateUsedSample[index] = |
| 212 _isolateUsedSamples.last[index] + _isolateFreeSamples.last[index]; |
| 213 isolateFreeSample[index] = 0; |
| 214 } else { |
| 215 isolateUsedSample[index] = _used(isolate); |
| 216 isolateFreeSample[index] = _free(isolate); |
| 217 } |
| 218 }); |
| 219 _isolateUsedSamples.add(isolateUsedSample); |
| 220 _isolateFreeSamples.add(isolateFreeSample); |
| 221 |
| 222 _vmSamples.add(vm.heapAllocatedMemoryUsage); |
| 223 |
| 224 _ts.add(now); |
| 225 } |
| 226 final List<int> isolateUsedSample = new List<int>.filled(length, 0); |
| 227 final List<int> isolateFreeSample = new List<int>.filled(length, 0); |
| 228 isolates.forEach((M.Isolate isolate) { |
| 229 _isolateName[isolate.id] = isolate.name; |
| 230 final index = _isolateIndex[isolate.id]; |
| 231 isolateUsedSample[index] = _used(isolate); |
| 232 isolateFreeSample[index] = _free(isolate); |
| 233 }); |
| 234 _isolateUsedSamples.add(isolateUsedSample); |
| 235 _isolateFreeSamples.add(isolateFreeSample); |
| 236 |
| 237 _vmSamples.add(vm.heapAllocatedMemoryUsage); |
| 238 |
| 239 _ts.add(now); |
| 240 _r.dirty(); |
| 241 _running = false; |
| 242 } |
| 243 |
| 244 void _handleEvent(records) => records.forEach((record) { |
| 245 if (record is ChartSelectionChangeRecord) { |
| 246 var selected = record.add; |
| 247 if (selected == null) { |
| 248 if (selected != _selected) { |
| 249 _onIsolateSelected.add(const IsolateSelectedEvent()); |
| 250 _r.dirty(); |
| 251 } |
| 252 } else { |
| 253 if (selected == 1) { |
| 254 if (selected != _selected) { |
| 255 _onIsolateSelected.add(const IsolateSelectedEvent()); |
| 256 _r.dirty(); |
| 257 } |
| 258 } else { |
| 259 selected -= selected % 2; |
| 260 if (selected != _selected) { |
| 261 _onIsolateSelected |
| 262 .add(new IsolateSelectedEvent(_seenIsolates[selected - 2])); |
| 263 _r.dirty(); |
| 264 } |
| 265 } |
| 266 } |
| 267 _selected = selected; |
| 268 _previewed = null; |
| 269 _hovered = null; |
| 270 } else if (record is ChartPreviewChangeRecord) { |
| 271 _previewed = record.previewed; |
| 272 } else if (record is ChartHoverChangeRecord) { |
| 273 _hovered = record.hovered; |
| 274 } |
| 275 }); |
| 276 |
| 277 int _used(M.Isolate i) => i.newSpace.used + i.oldSpace.used; |
| 278 int _capacity(M.Isolate i) => i.newSpace.capacity + i.oldSpace.capacity; |
| 279 int _free(M.Isolate i) => _capacity(i) - _used(i); |
| 280 |
| 281 String _label(String isolateId) { |
| 282 final index = _isolateIndex[isolateId]; |
| 283 final name = _isolateName[isolateId]; |
| 284 final free = _isolateFreeSamples.last[index]; |
| 285 final used = _isolateUsedSamples.last[index]; |
| 286 final usedStr = Utils.formatSize(used); |
| 287 final capacity = free + used; |
| 288 final capacityStr = Utils.formatSize(capacity); |
| 289 return '${name} ($usedStr / $capacityStr)'; |
| 290 } |
| 291 |
| 292 Element _formatNativeOvercard(int row) => new DivElement() |
| 293 ..children = [ |
| 294 new DivElement() |
| 295 ..classes = ['hovercard-title'] |
| 296 ..text = 'Native', |
| 297 new DivElement() |
| 298 ..classes = ['hovercard-measure', 'hovercard-multi'] |
| 299 ..children = [ |
| 300 new DivElement() |
| 301 ..classes = ['hovercard-measure-label'] |
| 302 ..text = 'Heap', |
| 303 new DivElement() |
| 304 ..classes = ['hovercard-measure-value'] |
| 305 ..text = Utils.formatSize(_vmSamples[row]), |
| 306 ] |
| 307 ]; |
| 308 |
| 309 Element _formatIsolateOvercard(String isolateId, int row) { |
| 310 final index = _isolateIndex[isolateId]; |
| 311 final free = _isolateFreeSamples[row][index]; |
| 312 final used = _isolateUsedSamples[row][index]; |
| 313 final capacity = free + used; |
| 314 return new DivElement() |
| 315 ..children = [ |
| 316 new DivElement() |
| 317 ..classes = ['hovercard-title'] |
| 318 ..text = _isolateName[isolateId], |
| 319 new DivElement() |
| 320 ..classes = ['hovercard-measure', 'hovercard-multi'] |
| 321 ..children = [ |
| 322 new DivElement() |
| 323 ..classes = ['hovercard-measure-label'] |
| 324 ..text = 'Heap Capacity', |
| 325 new DivElement() |
| 326 ..classes = ['hovercard-measure-value'] |
| 327 ..text = Utils.formatSize(capacity), |
| 328 ], |
| 329 new DivElement() |
| 330 ..classes = ['hovercard-measure', 'hovercard-multi'] |
| 331 ..children = [ |
| 332 new DivElement() |
| 333 ..classes = ['hovercard-measure-label'] |
| 334 ..text = 'Free Heap', |
| 335 new DivElement() |
| 336 ..classes = ['hovercard-measure-value'] |
| 337 ..text = Utils.formatSize(free), |
| 338 ], |
| 339 new DivElement() |
| 340 ..classes = ['hovercard-measure', 'hovercard-multi'] |
| 341 ..children = [ |
| 342 new DivElement() |
| 343 ..classes = ['hovercard-measure-label'] |
| 344 ..text = 'Used Heap', |
| 345 new DivElement() |
| 346 ..classes = ['hovercard-measure-value'] |
| 347 ..text = Utils.formatSize(used), |
| 348 ] |
| 349 ]; |
| 350 } |
| 351 } |
| 352 |
| 353 class MemoryChartTheme extends QuantumChartTheme { |
| 354 final int _offset; |
| 355 |
| 356 MemoryChartTheme(int offset) : _offset = offset { |
| 357 assert(offset != null); |
| 358 assert(offset >= 0); |
| 359 } |
| 360 |
| 361 @override |
| 362 String getColorForKey(key, [int state = 0]) { |
| 363 key -= 1; |
| 364 if (key > _offset) { |
| 365 key = _offset + (key - _offset) ~/ 2; |
| 366 } |
| 367 key += 1; |
| 368 return super.getColorForKey(key, state); |
| 369 } |
| 370 |
| 371 @override |
| 372 String getFilterForState(int state) => state & ChartState.COL_PREVIEW != 0 || |
| 373 state & ChartState.VAL_HOVERED != 0 || |
| 374 state & ChartState.COL_SELECTED != 0 || |
| 375 state & ChartState.VAL_HIGHLIGHTED != 0 |
| 376 ? 'url(#drop-shadow)' |
| 377 : ''; |
| 378 |
| 379 @override |
| 380 String get filters => |
| 381 '<defs>' + |
| 382 super.filters + |
| 383 ''' |
| 384 <filter id="stroke-grid" primitiveUnits="userSpaceOnUse"> |
| 385 <feFlood in="SourceGraphic" x="0" y="0" width="4" height="4" |
| 386 flood-color="black" flood-opacity="0.2" result='Black'/> |
| 387 <feFlood in="SourceGraphic" x="1" y="1" width="3" height="3" |
| 388 flood-color="black" flood-opacity="0.8" result='White'/> |
| 389 <feComposite in="Black" in2="White" operator="xor" x="0" y="0" width="4" hei
ght="4"/> |
| 390 <feTile x="0" y="0" width="100%" height="100%" /> |
| 391 <feComposite in2="SourceAlpha" result="Pattern" operator="in" x="0" y="0" wi
dth="100%" height="100%"/> |
| 392 <feComposite in="SourceGraphic" in2="Pattern" operator="atop" x="0" y="0" wi
dth="100%" height="100%"/> |
| 393 </filter> |
| 394 </defs> |
| 395 '''; |
| 396 |
| 397 StyleElement get style => new StyleElement() |
| 398 ..text = ''' |
| 399 memory-graph svg .stacked-line-rdr-line:nth-child(2n+${_offset+1}) |
| 400 path:nth-child(1) { |
| 401 filter: url(#stroke-grid); |
| 402 }'''; |
| 403 } |
OLD | NEW |