| // 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; |
| } |