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