| // 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. |
| |
| library heap_map_element; |
| |
| import 'dart:async'; |
| import 'dart:html'; |
| import 'dart:math'; |
| import 'package:observatory/models.dart' as M; |
| import 'package:observatory/service.dart' as S; |
| import 'package:observatory/src/elements/helpers/rendering_scheduler.dart'; |
| import 'package:observatory/src/elements/helpers/nav_bar.dart'; |
| import 'package:observatory/src/elements/helpers/nav_menu.dart'; |
| import 'package:observatory/src/elements/helpers/custom_element.dart'; |
| import 'package:observatory/src/elements/helpers/uris.dart'; |
| import 'package:observatory/src/elements/nav/isolate_menu.dart'; |
| import 'package:observatory/src/elements/nav/notify.dart'; |
| import 'package:observatory/src/elements/nav/refresh.dart'; |
| import 'package:observatory/src/elements/nav/top_menu.dart'; |
| import 'package:observatory/src/elements/nav/vm_menu.dart'; |
| |
| class HeapMapElement extends CustomElement implements Renderable { |
| late RenderingScheduler<HeapMapElement> _r; |
| |
| Stream<RenderedEvent<HeapMapElement>> get onRendered => _r.onRendered; |
| |
| late M.VM _vm; |
| late M.IsolateRef _isolate; |
| late M.EventRepository _events; |
| late M.NotificationRepository _notifications; |
| M.VMRef get vm => _vm; |
| M.IsolateRef get isolate => _isolate; |
| M.NotificationRepository get notifications => _notifications; |
| |
| factory HeapMapElement(M.VM vm, M.IsolateRef isolate, |
| M.EventRepository events, M.NotificationRepository notifications, |
| {RenderingQueue? queue}) { |
| assert(vm != null); |
| assert(isolate != null); |
| assert(events != null); |
| assert(notifications != null); |
| HeapMapElement e = new HeapMapElement.created(); |
| e._r = new RenderingScheduler<HeapMapElement>(e, queue: queue); |
| e._vm = vm; |
| e._isolate = isolate; |
| e._events = events; |
| e._notifications = notifications; |
| return e; |
| } |
| |
| HeapMapElement.created() : super.created('heap-map'); |
| |
| @override |
| attached() { |
| super.attached(); |
| _r.enable(); |
| _refresh(); |
| } |
| |
| @override |
| detached() { |
| super.detached(); |
| _r.disable(notify: true); |
| children = <Element>[]; |
| } |
| |
| CanvasElement? _canvas; |
| dynamic _fragmentationData; |
| int? _pageHeight; |
| final _classIdToColor = {}; |
| final _colorToClassId = {}; |
| final _classIdToName = {}; |
| |
| static final _freeColor = [255, 255, 255, 255]; |
| static final _pageSeparationColor = [0, 0, 0, 255]; |
| static const _PAGE_SEPARATION_HEIGHT = 4; |
| // Many browsers will not display a very tall canvas. |
| // TODO(koda): Improve interface for huge heaps. |
| static const _MAX_CANVAS_HEIGHT = 6000; |
| |
| String _status = 'Loading'; |
| S.ServiceMap? _fragmentation; |
| |
| void render() { |
| if (_canvas == null) { |
| _canvas = new CanvasElement() |
| ..width = 1 |
| ..height = 1 |
| ..onMouseMove.listen(_handleMouseMove); |
| } |
| |
| // Set hover text to describe the object under the cursor. |
| _canvas!.title = _status; |
| |
| children = <Element>[ |
| navBar(<Element>[ |
| new NavTopMenuElement(queue: _r.queue).element, |
| new NavVMMenuElement(_vm, _events, queue: _r.queue).element, |
| new NavIsolateMenuElement(_isolate, _events, queue: _r.queue).element, |
| navMenu('heap map'), |
| (new NavRefreshElement(label: 'Mark-Compact', queue: _r.queue) |
| ..onRefresh.listen((_) => _refresh(gc: "mark-compact"))) |
| .element, |
| (new NavRefreshElement(label: 'Mark-Sweep', queue: _r.queue) |
| ..onRefresh.listen((_) => _refresh(gc: "mark-sweep"))) |
| .element, |
| (new NavRefreshElement(label: 'Scavenge', queue: _r.queue) |
| ..onRefresh.listen((_) => _refresh(gc: "scavenge"))) |
| .element, |
| (new NavRefreshElement(queue: _r.queue) |
| ..onRefresh.listen((_) => _refresh())) |
| .element, |
| (new NavNotifyElement(_notifications, queue: _r.queue)).element |
| ]), |
| new DivElement() |
| ..classes = ['content-centered-big'] |
| ..children = <Element>[ |
| new HeadingElement.h2()..text = _status, |
| new HRElement(), |
| ], |
| new DivElement() |
| ..classes = ['flex-row'] |
| ..children = <Element>[_canvas!] |
| ]; |
| } |
| |
| // Encode color as single integer, to enable using it as a map key. |
| int _packColor(Iterable<int> color) { |
| int packed = 0; |
| for (var component in color) { |
| packed = packed * 256 + component; |
| } |
| return packed; |
| } |
| |
| void _addClass(int classId, String name, Iterable<int> color) { |
| _classIdToName[classId] = name.split('@')[0]; |
| _classIdToColor[classId] = color; |
| _colorToClassId[_packColor(color)] = classId; |
| } |
| |
| void _updateClassList(classList, int freeClassId) { |
| for (var member in classList['classes']) { |
| if (member is! S.Class) { |
| // TODO(turnidge): The printing for some of these non-class |
| // members is broken. Fix this: |
| // |
| // Logger.root.info('$member'); |
| print('Ignoring non-class in class list'); |
| continue; |
| } |
| var classId = int.parse(member.id!.split('/').last); |
| var color = _classIdToRGBA(classId); |
| _addClass(classId, member.name!, color); |
| } |
| _addClass(freeClassId, 'Free', _freeColor); |
| _addClass(0, '', _pageSeparationColor); |
| } |
| |
| Iterable<int> _classIdToRGBA(int classId) { |
| // TODO(koda): Pick random hue, but fixed saturation and value. |
| var rng = new Random(classId); |
| return [rng.nextInt(128), rng.nextInt(128), rng.nextInt(128), 255]; |
| } |
| |
| String _classNameAt(Point<num> point) { |
| var color = new PixelReference(_fragmentationData, point).color; |
| return _classIdToName[_colorToClassId[_packColor(color)]]; |
| } |
| |
| ObjectInfo? _objectAt(Point<num> point) { |
| if (_fragmentation == null || _canvas == null) { |
| return null; |
| } |
| var pagePixels = _pageHeight! * _fragmentationData.width; |
| var index = new PixelReference(_fragmentationData, point).index; |
| var pageIndex = index ~/ pagePixels; |
| num pageOffset = index % pagePixels; |
| var pages = _fragmentation!['pages']; |
| if (pageIndex < 0 || pageIndex >= pages.length) { |
| return null; |
| } |
| // Scan the page to find start and size. |
| var page = pages[pageIndex]; |
| var objects = page['objects']; |
| var offset = 0; |
| var size = 0; |
| for (var i = 0; i < objects.length; i += 2) { |
| size = objects[i]; |
| offset += size; |
| if (offset > pageOffset) { |
| pageOffset = offset - size; |
| break; |
| } |
| } |
| return new ObjectInfo( |
| int.parse(page['objectStart']) + |
| pageOffset * _fragmentation!['unitSizeBytes'], |
| size * _fragmentation!['unitSizeBytes']); |
| } |
| |
| void _handleMouseMove(MouseEvent event) { |
| var info = _objectAt(event.offset); |
| if (info == null) { |
| _status = ''; |
| _r.dirty(); |
| return; |
| } |
| var addressString = '${info.size}B @ 0x${info.address.toRadixString(16)}'; |
| var className = _classNameAt(event.offset); |
| _status = (className == '') ? '-' : '$className $addressString'; |
| _r.dirty(); |
| } |
| |
| void _updateFragmentationData() { |
| if (_fragmentation == null || _canvas == null) { |
| return; |
| } |
| _updateClassList( |
| _fragmentation!['classList'], _fragmentation!['freeClassId']); |
| var pages = _fragmentation!['pages']; |
| var width = max(_canvas!.parent!.client.width, 1) as int; |
| _pageHeight = _PAGE_SEPARATION_HEIGHT + |
| (_fragmentation!['pageSizeBytes'] as int) ~/ |
| (_fragmentation!['unitSizeBytes'] as int) ~/ |
| width; |
| var height = min(_pageHeight! * pages.length, _MAX_CANVAS_HEIGHT) as int; |
| _fragmentationData = _canvas!.context2D.createImageData(width, height); |
| _canvas!.width = _fragmentationData.width; |
| _canvas!.height = _fragmentationData.height; |
| _renderPages(0); |
| } |
| |
| // Renders and draws asynchronously, one page at a time to avoid |
| // blocking the UI. |
| void _renderPages(int startPage) { |
| var pages = _fragmentation!['pages']; |
| _status = 'Loaded $startPage of ${pages.length} pages'; |
| _r.dirty(); |
| var startY = (startPage * _pageHeight!).round(); |
| var endY = startY + _pageHeight!.round(); |
| if (startPage >= pages.length || endY > _fragmentationData.height) { |
| return; |
| } |
| var pixel = new PixelReference(_fragmentationData, new Point(0, startY)); |
| var objects = pages[startPage]['objects']; |
| for (var i = 0; i < objects.length; i += 2) { |
| var count = objects[i]; |
| var classId = objects[i + 1]; |
| var color = _classIdToColor[classId]; |
| while (count-- > 0) { |
| pixel.color = color; |
| pixel = pixel.next(); |
| } |
| } |
| while (pixel.point.y < endY) { |
| pixel.color = _pageSeparationColor; |
| pixel = pixel.next(); |
| } |
| _canvas!.context2D.putImageData( |
| _fragmentationData, 0, 0, 0, startY, _fragmentationData.width, endY); |
| // Continue with the next page, asynchronously. |
| new Future(() { |
| _renderPages(startPage + 1); |
| }); |
| } |
| |
| Future _refresh({String? gc}) { |
| final isolate = _isolate as S.Isolate; |
| var params = {}; |
| if (gc != null) { |
| params['gc'] = gc; |
| } |
| return isolate.invokeRpc('_getHeapMap', params).then((serviceObject) { |
| S.ServiceMap response = serviceObject as S.ServiceMap; |
| assert(response['type'] == 'HeapMap'); |
| _fragmentation = response; |
| _updateFragmentationData(); |
| }); |
| } |
| } |
| |
| // A reference to a particular pixel of ImageData. |
| class PixelReference { |
| final _data; |
| var _dataIndex; |
| static const NUM_COLOR_COMPONENTS = 4; |
| |
| PixelReference(ImageData data, Point<num> point) |
| : _data = data, |
| _dataIndex = (point.y * data.width + point.x) * NUM_COLOR_COMPONENTS; |
| |
| PixelReference._fromDataIndex(this._data, this._dataIndex); |
| |
| Point<num> get point => new Point(index % _data.width, index ~/ _data.width); |
| |
| void set color(Iterable<int> color) { |
| _data.data.setRange(_dataIndex, _dataIndex + NUM_COLOR_COMPONENTS, color); |
| } |
| |
| Iterable<int> get color => |
| _data.data.getRange(_dataIndex, _dataIndex + NUM_COLOR_COMPONENTS); |
| |
| // Returns the next pixel in row-major order. |
| PixelReference next() => new PixelReference._fromDataIndex( |
| _data, _dataIndex + NUM_COLOR_COMPONENTS); |
| |
| // The row-major index of this pixel. |
| int get index => _dataIndex ~/ NUM_COLOR_COMPONENTS; |
| } |
| |
| class ObjectInfo { |
| final address; |
| final size; |
| ObjectInfo(this.address, this.size); |
| } |