| // Copyright (c) 2014, 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. | |
| import 'dart:async'; | |
| import 'package:web/web.dart'; | |
| import '../../models.dart' as M; | |
| import 'class_ref.dart'; | |
| import 'containers/virtual_collection.dart'; | |
| import 'helpers/custom_element.dart'; | |
| import 'helpers/element_utils.dart'; | |
| import 'helpers/nav_bar.dart'; | |
| import 'helpers/nav_menu.dart'; | |
| import 'helpers/rendering_scheduler.dart'; | |
| import 'nav/isolate_menu.dart'; | |
| import 'nav/notify.dart'; | |
| import 'nav/refresh.dart'; | |
| import 'nav/top_menu.dart'; | |
| import 'nav/vm_menu.dart'; | |
| import '../../utils.dart'; | |
| enum _SortingField { | |
| newInstances, | |
| newInternalSize, | |
| newExternalSize, | |
| newSize, | |
| oldInstances, | |
| oldInternalSize, | |
| oldExternalSize, | |
| oldSize, | |
| instances, | |
| internalSize, | |
| externalSize, | |
| size, | |
| className, | |
| } | |
| enum _SortingDirection { ascending, descending } | |
| class AllocationProfileElement extends CustomElement implements Renderable { | |
| late RenderingScheduler<AllocationProfileElement> _r; | |
| Stream<RenderedEvent<AllocationProfileElement>> get onRendered => | |
| _r.onRendered; | |
| late M.VM _vm; | |
| late M.IsolateRef _isolate; | |
| late M.EventRepository _events; | |
| late M.NotificationRepository _notifications; | |
| late M.AllocationProfileRepository _repository; | |
| M.AllocationProfile? _profile; | |
| bool _autoRefresh = false; | |
| bool _isCompacted = false; | |
| late StreamSubscription _gcSubscription; | |
| _SortingField _sortingField = _SortingField.size; | |
| _SortingDirection _sortingDirection = _SortingDirection.descending; | |
| M.VMRef get vm => _vm; | |
| M.IsolateRef get isolate => _isolate; | |
| M.NotificationRepository get notifications => _notifications; | |
| factory AllocationProfileElement( | |
| M.VM vm, | |
| M.IsolateRef isolate, | |
| M.EventRepository events, | |
| M.NotificationRepository notifications, | |
| M.AllocationProfileRepository repository, { | |
| RenderingQueue? queue, | |
| }) { | |
| AllocationProfileElement e = new AllocationProfileElement.created(); | |
| e._r = new RenderingScheduler<AllocationProfileElement>(e, queue: queue); | |
| e._vm = vm; | |
| e._isolate = isolate; | |
| e._events = events; | |
| e._notifications = notifications; | |
| e._repository = repository; | |
| return e; | |
| } | |
| AllocationProfileElement.created() : super.created('allocation-profile'); | |
| @override | |
| attached() { | |
| super.attached(); | |
| _r.enable(); | |
| _refresh(); | |
| _gcSubscription = _events.onGCEvent.listen((e) { | |
| if (_autoRefresh && (e.isolate.id == _isolate.id)) { | |
| _refresh(); | |
| } | |
| }); | |
| } | |
| @override | |
| detached() { | |
| super.detached(); | |
| _r.disable(notify: true); | |
| removeChildren(); | |
| _gcSubscription.cancel(); | |
| } | |
| void render() { | |
| children = <HTMLElement>[ | |
| navBar(<HTMLElement>[ | |
| new NavTopMenuElement(queue: _r.queue).element, | |
| new NavVMMenuElement(_vm, _events, queue: _r.queue).element, | |
| new NavIsolateMenuElement(_isolate, _events, queue: _r.queue).element, | |
| navMenu('allocation profile'), | |
| (new NavRefreshElement( | |
| label: 'Download', | |
| disabled: _profile == null, | |
| queue: _r.queue, | |
| )..onRefresh.listen((_) => _downloadCSV())).element, | |
| (new NavRefreshElement( | |
| label: 'GC', | |
| queue: _r.queue, | |
| )..onRefresh.listen((_) => _refresh(gc: true))).element, | |
| (new NavRefreshElement( | |
| queue: _r.queue, | |
| )..onRefresh.listen((_) => _refresh())).element, | |
| new HTMLDivElement() | |
| ..className = 'nav-option' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLInputElement() | |
| ..id = 'allocation-profile-auto-refresh' | |
| ..type = 'checkbox' | |
| ..checked = _autoRefresh | |
| ..onChange.listen((_) => _autoRefresh = !_autoRefresh), | |
| new HTMLLabelElement() | |
| ..htmlFor = 'allocation-profile-auto-refresh' | |
| ..textContent = 'Auto-refresh on GC', | |
| ]), | |
| new NavNotifyElement(_notifications, queue: _r.queue).element, | |
| ]), | |
| new HTMLDivElement() | |
| ..className = 'content-centered-big' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLHeadingElement.h2()..textContent = 'Allocation Profile', | |
| new HTMLHRElement(), | |
| ]), | |
| ]; | |
| if (_profile == null) { | |
| appendChild( | |
| new HTMLDivElement() | |
| ..className = 'content-centered-big' | |
| ..append(new HTMLHeadingElement.h2()..textContent = 'Loading...'), | |
| ); | |
| } else { | |
| appendChildren([ | |
| new HTMLDivElement() | |
| ..className = 'content-centered-big' | |
| ..appendChildren( | |
| _isCompacted | |
| ? [] | |
| : [ | |
| new HTMLDivElement() | |
| ..className = 'memberList' | |
| ..append( | |
| new HTMLDivElement() | |
| ..className = 'memberItem' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLDivElement() | |
| ..className = 'memberName' | |
| ..textContent = 'last forced GC at', | |
| new HTMLDivElement() | |
| ..className = 'memberValue' | |
| ..textContent = _profile!.lastServiceGC == null | |
| ? '---' | |
| : '${_profile!.lastServiceGC}', | |
| ]), | |
| ), | |
| new HTMLHRElement(), | |
| ], | |
| ), | |
| new HTMLDivElement() | |
| ..className = 'content-centered-big compactable' | |
| ..appendChildren([ | |
| new HTMLDivElement() | |
| ..className = 'heap-space left' | |
| ..appendChildren( | |
| _isCompacted | |
| ? [ | |
| new HTMLHeadingElement.h2() | |
| ..textContent = | |
| 'New Generation ' | |
| '(${_usedCaption(_profile!.newSpace)})', | |
| ] | |
| : [ | |
| new HTMLHeadingElement.h2() | |
| ..textContent = 'New Generation', | |
| new HTMLBRElement(), | |
| new HTMLDivElement() | |
| ..className = 'memberList' | |
| ..appendChildren( | |
| _createSpaceMembers(_profile!.newSpace), | |
| ), | |
| ], | |
| ), | |
| new HTMLDivElement() | |
| ..className = 'heap-space left' | |
| ..appendChildren( | |
| _isCompacted | |
| ? [ | |
| new HTMLHeadingElement.h2() | |
| ..textContent = | |
| 'Old Generation ' | |
| '(${_usedCaption(_profile!.oldSpace)})', | |
| ] | |
| : [ | |
| new HTMLHeadingElement.h2() | |
| ..textContent = 'Old Generation', | |
| new HTMLBRElement(), | |
| new HTMLDivElement() | |
| ..className = 'memberList' | |
| ..appendChildren( | |
| _createSpaceMembers(_profile!.oldSpace), | |
| ), | |
| ], | |
| ), | |
| new HTMLDivElement() | |
| ..className = 'heap-space left' | |
| ..appendChildren( | |
| _isCompacted | |
| ? [ | |
| new HTMLHeadingElement.h2() | |
| ..textContent = | |
| 'Total ' | |
| '(${_usedCaption(_profile!.totalSpace)})', | |
| ] | |
| : [ | |
| new HTMLHeadingElement.h2()..textContent = 'Total', | |
| new HTMLBRElement(), | |
| new HTMLDivElement() | |
| ..className = 'memberList' | |
| ..appendChildren( | |
| _createSpaceMembers(_profile!.totalSpace), | |
| ), | |
| ], | |
| ), | |
| new HTMLButtonElement() | |
| ..className = 'compact' | |
| ..textContent = _isCompacted ? 'expand â–¼' : 'compact â–²' | |
| ..onClick.listen((_) { | |
| _isCompacted = !_isCompacted; | |
| _r.dirty(); | |
| }), | |
| new HTMLHRElement(), | |
| ]), | |
| new HTMLDivElement() | |
| ..className = _isCompacted ? 'collection expanded' : 'collection' | |
| ..appendChild( | |
| new VirtualCollectionElement( | |
| _createCollectionLine, | |
| _updateCollectionLine, | |
| createHeader: _createCollectionHeader, | |
| search: _search, | |
| items: _profile!.members.toList()..sort(_createSorter()), | |
| queue: _r.queue, | |
| ).element, | |
| ), | |
| ]); | |
| } | |
| } | |
| _createSorter() { | |
| var getter; | |
| switch (_sortingField) { | |
| case _SortingField.newInternalSize: | |
| getter = _getNewInternalSize; | |
| break; | |
| case _SortingField.newExternalSize: | |
| getter = _getNewExternalSize; | |
| break; | |
| case _SortingField.newSize: | |
| getter = _getNewSize; | |
| break; | |
| case _SortingField.newInstances: | |
| getter = _getNewInstances; | |
| break; | |
| case _SortingField.oldInternalSize: | |
| getter = _getOldInternalSize; | |
| break; | |
| case _SortingField.oldExternalSize: | |
| getter = _getOldExternalSize; | |
| break; | |
| case _SortingField.oldSize: | |
| getter = _getOldSize; | |
| break; | |
| case _SortingField.oldInstances: | |
| getter = _getOldInstances; | |
| break; | |
| case _SortingField.internalSize: | |
| getter = _getInternalSize; | |
| break; | |
| case _SortingField.externalSize: | |
| getter = _getExternalSize; | |
| break; | |
| case _SortingField.size: | |
| getter = _getSize; | |
| break; | |
| case _SortingField.instances: | |
| getter = _getInstances; | |
| break; | |
| case _SortingField.className: | |
| getter = (M.ClassHeapStats s) => s.clazz!.name; | |
| break; | |
| } | |
| switch (_sortingDirection) { | |
| case _SortingDirection.ascending: | |
| int sort(M.ClassHeapStats a, M.ClassHeapStats b) { | |
| return getter(a).compareTo(getter(b)); | |
| } | |
| return sort; | |
| case _SortingDirection.descending: | |
| int sort(M.ClassHeapStats a, M.ClassHeapStats b) { | |
| return getter(b).compareTo(getter(a)); | |
| } | |
| return sort; | |
| } | |
| } | |
| static HTMLElement _createCollectionLine() => new HTMLDivElement() | |
| ..className = 'collection-item' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'instances' | |
| ..textContent = '0', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'instances' | |
| ..textContent = '0', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'bytes' | |
| ..textContent = '0B', | |
| new HTMLSpanElement() | |
| ..className = 'instances' | |
| ..textContent = '0', | |
| new HTMLSpanElement()..className = 'name', | |
| ]); | |
| List<HTMLElement> _createCollectionHeader() => [ | |
| new HTMLDivElement() | |
| ..className = 'collection-item' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLSpanElement() | |
| ..className = 'group' | |
| ..textContent = 'New Generation', | |
| new HTMLSpanElement() | |
| ..className = 'group' | |
| ..textContent = 'Old Generation', | |
| new HTMLSpanElement() | |
| ..className = 'group' | |
| ..textContent = 'Total', | |
| new HTMLSpanElement() | |
| ..className = 'group' | |
| ..textContent = '', | |
| ]), | |
| new HTMLDivElement() | |
| ..className = 'collection-item' | |
| ..appendChildren(<Node>[ | |
| _createHeaderButton( | |
| 'bytes', | |
| 'Internal', | |
| _SortingField.newInternalSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'External', | |
| _SortingField.newExternalSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'Size', | |
| _SortingField.newSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'instances', | |
| 'Instances', | |
| _SortingField.newInstances, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'Internal', | |
| _SortingField.oldInternalSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'External', | |
| _SortingField.oldExternalSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'Size', | |
| _SortingField.oldSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'instances', | |
| 'Instances', | |
| _SortingField.oldInstances, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'Internal', | |
| _SortingField.internalSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'External', | |
| _SortingField.externalSize, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'bytes', | |
| 'Size', | |
| _SortingField.size, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'instances', | |
| 'Instances', | |
| _SortingField.instances, | |
| _SortingDirection.descending, | |
| ), | |
| _createHeaderButton( | |
| 'name', | |
| 'Class', | |
| _SortingField.className, | |
| _SortingDirection.ascending, | |
| ), | |
| ]), | |
| ]; | |
| HTMLButtonElement _createHeaderButton( | |
| String className, | |
| String text, | |
| _SortingField field, | |
| _SortingDirection direction, | |
| ) => new HTMLButtonElement() | |
| ..className = className | |
| ..textContent = _sortingField != field | |
| ? text | |
| : _sortingDirection == _SortingDirection.ascending | |
| ? '$textâ–¼' | |
| : '$textâ–²' | |
| ..onClick.listen((_) => _setSorting(field, direction)); | |
| void _setSorting(_SortingField field, _SortingDirection defaultDirection) { | |
| if (_sortingField == field) { | |
| switch (_sortingDirection) { | |
| case _SortingDirection.descending: | |
| _sortingDirection = _SortingDirection.ascending; | |
| break; | |
| case _SortingDirection.ascending: | |
| _sortingDirection = _SortingDirection.descending; | |
| break; | |
| } | |
| } else { | |
| _sortingDirection = defaultDirection; | |
| _sortingField = field; | |
| } | |
| _r.dirty(); | |
| } | |
| void _updateCollectionLine(HTMLElement e, itemDynamic, index) { | |
| M.ClassHeapStats item = itemDynamic; | |
| e.childNodes.item(0)!.textContent = Utils.formatSize( | |
| _getNewInternalSize(item), | |
| ); | |
| e.childNodes.item(1)!.textContent = Utils.formatSize( | |
| _getNewExternalSize(item), | |
| ); | |
| e.childNodes.item(2)!.textContent = Utils.formatSize(_getNewSize(item)); | |
| e.childNodes.item(3)!.textContent = '${_getNewInstances(item)}'; | |
| e.childNodes.item(4)!.textContent = Utils.formatSize( | |
| _getOldInternalSize(item), | |
| ); | |
| e.childNodes.item(5)!.textContent = Utils.formatSize( | |
| _getOldExternalSize(item), | |
| ); | |
| e.childNodes.item(6)!.textContent = Utils.formatSize(_getOldSize(item)); | |
| e.childNodes.item(7)!.textContent = '${_getOldInstances(item)}'; | |
| e.childNodes.item(8)!.textContent = Utils.formatSize( | |
| _getInternalSize(item), | |
| ); | |
| e.childNodes.item(9)!.textContent = Utils.formatSize( | |
| _getExternalSize(item), | |
| ); | |
| e.childNodes.item(10)!.textContent = Utils.formatSize(_getSize(item)); | |
| e.childNodes.item(11)!.textContent = '${_getInstances(item)}'; | |
| final old = e.childNodes.item(12)!; | |
| e.insertBefore( | |
| old, | |
| e.appendChild( | |
| (new ClassRefElement(_isolate, item.clazz!, queue: _r.queue).element | |
| ..className = 'name'), | |
| ), | |
| ); | |
| e.removeChild(old); | |
| } | |
| bool _search(Pattern pattern, itemDynamic) { | |
| M.ClassHeapStats item = itemDynamic; | |
| return item.clazz!.name!.contains(pattern); | |
| } | |
| static String _usedCaption(M.HeapSpace space) => | |
| '${Utils.formatSize(space.used)}' | |
| ' of ' | |
| '${Utils.formatSize(space.capacity)}'; | |
| static List<HTMLElement> _createSpaceMembers(M.HeapSpace space) { | |
| final used = _usedCaption(space); | |
| final ext = '${Utils.formatSize(space.external)}'; | |
| final collections = '${space.collections}'; | |
| final avgCollectionTime = | |
| '${Utils.formatDurationInMilliseconds(space.avgCollectionTime)} ms'; | |
| return [ | |
| new HTMLDivElement() | |
| ..className = 'memberItem' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLDivElement() | |
| ..className = 'memberName' | |
| ..textContent = 'used', | |
| new HTMLDivElement() | |
| ..className = 'memberValue' | |
| ..textContent = used, | |
| ]), | |
| new HTMLDivElement() | |
| ..className = 'memberItem' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLDivElement() | |
| ..className = 'memberName' | |
| ..textContent = 'external', | |
| new HTMLDivElement() | |
| ..className = 'memberValue' | |
| ..textContent = ext, | |
| ]), | |
| new HTMLDivElement() | |
| ..className = 'memberItem' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLDivElement() | |
| ..className = 'memberName' | |
| ..textContent = 'collections', | |
| new HTMLDivElement() | |
| ..className = 'memberValue' | |
| ..textContent = collections, | |
| ]), | |
| new HTMLDivElement() | |
| ..className = 'memberItem' | |
| ..appendChildren(<HTMLElement>[ | |
| new HTMLDivElement() | |
| ..className = 'memberName' | |
| ..textContent = 'average collection time', | |
| new HTMLDivElement() | |
| ..className = 'memberValue' | |
| ..textContent = avgCollectionTime, | |
| ]), | |
| ]; | |
| } | |
| Future _refresh({bool gc = false, bool reset = false}) async { | |
| _profile = null; | |
| _r.dirty(); | |
| _profile = await _repository.get(_isolate, gc: gc, reset: reset); | |
| _r.dirty(); | |
| } | |
| void _downloadCSV() { | |
| assert(_profile != null); | |
| final header = | |
| [ | |
| '"New Internal"', | |
| '"New External"', | |
| '"New Size"', | |
| '"New Instances"', | |
| '"Old Internal"', | |
| '"Old External"', | |
| '"Old Size"', | |
| '"Old Instances"', | |
| '"Internal"', | |
| '"External"', | |
| '"Size"', | |
| '"Instances"', | |
| 'Class', | |
| ].join(',') + | |
| '\n'; | |
| HTMLAnchorElement tl = document.createElement('a') as HTMLAnchorElement; | |
| tl | |
| ..attributes.setNamedItem( | |
| document.createAttribute('href') | |
| ..value = | |
| 'data:text/plain;charset=utf-8,' + | |
| Uri.encodeComponent( | |
| header + | |
| (_profile!.members.toList()..sort(_createSorter())) | |
| .map(_csvOut) | |
| .join('\n'), | |
| ), | |
| ) | |
| ..attributes.setNamedItem( | |
| document.createAttribute('download')..value = 'heap-profile.csv', | |
| ) | |
| ..click(); | |
| } | |
| static _csvOut(M.ClassHeapStats s) { | |
| return [ | |
| _getNewInternalSize(s), | |
| _getNewExternalSize(s), | |
| _getNewSize(s), | |
| _getNewInstances(s), | |
| _getOldInternalSize(s), | |
| _getOldExternalSize(s), | |
| _getOldSize(s), | |
| _getOldInstances(s), | |
| _getInternalSize(s), | |
| _getExternalSize(s), | |
| _getSize(s), | |
| _getInstances(s), | |
| s.clazz!.name, | |
| ].join(','); | |
| } | |
| static int _getNewInstances(M.ClassHeapStats s) => s.newSpace.instances; | |
| static int _getNewInternalSize(M.ClassHeapStats s) => s.newSpace.internalSize; | |
| static int _getNewExternalSize(M.ClassHeapStats s) => s.newSpace.externalSize; | |
| static int _getNewSize(M.ClassHeapStats s) => s.newSpace.size; | |
| static int _getOldInstances(M.ClassHeapStats s) => s.oldSpace.instances; | |
| static int _getOldInternalSize(M.ClassHeapStats s) => s.oldSpace.internalSize; | |
| static int _getOldExternalSize(M.ClassHeapStats s) => s.oldSpace.externalSize; | |
| static int _getOldSize(M.ClassHeapStats s) => s.oldSpace.size; | |
| static int _getInstances(M.ClassHeapStats s) => | |
| s.newSpace.instances + s.oldSpace.instances; | |
| static int _getInternalSize(M.ClassHeapStats s) => | |
| s.newSpace.internalSize + s.oldSpace.internalSize; | |
| static int _getExternalSize(M.ClassHeapStats s) => | |
| s.newSpace.externalSize + s.oldSpace.externalSize; | |
| static int _getSize(M.ClassHeapStats s) => s.newSpace.size + s.oldSpace.size; | |
| } |