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 |