blob: 6688e5a3da56f2306069a136430e91e7cdc0e084 [file] [log] [blame]
// 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 'observatory_element.dart';
import 'package:observatory/service.dart';
import 'package:logging/logging.dart';
import 'package:polymer/polymer.dart';
// A reference to a particular pixel of ImageData.
class PixelReference {
final _data;
var _dataIndex;
static const NUM_COLOR_COMPONENTS = 4;
PixelReference(ImageData data, Point<int> point)
: _data = data,
_dataIndex = (point.y * data.width + point.x) * NUM_COLOR_COMPONENTS;
PixelReference._fromDataIndex(this._data, this._dataIndex);
Point<int> 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);
}
@CustomTag('heap-map')
class HeapMapElement extends ObservatoryElement {
var _fragmentationCanvas;
var _fragmentationData;
var _pageHeight;
var _classIdToColor = {};
var _colorToClassId = {};
var _classIdToName = {};
static final _freeColor = [255, 255, 255, 255];
static final _pageSeparationColor = [0, 0, 0, 255];
static const _PAGE_SEPARATION_HEIGHT = 4;
@observable String status;
@published ServiceMap fragmentation;
HeapMapElement.created() : super.created() {
}
void enteredView() {
super.enteredView();
_fragmentationCanvas = shadowRoot.querySelector("#fragmentation");
_fragmentationCanvas.onMouseMove.listen(_handleMouseMove);
_fragmentationCanvas.onMouseDown.listen(_handleClick);
}
// 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['members']) {
if (member['type'] != '@Class') {
Logger.root.info('$member');
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<int> point) {
var color = new PixelReference(_fragmentationData, point).color;
return _classIdToName[_colorToClassId[_packColor(color)]];
}
ObjectInfo _objectAt(Point<int> point) {
var pagePixels = _pageHeight * _fragmentationData.width;
var index = new PixelReference(_fragmentationData, point).index;
var pageIndex = index ~/ pagePixels;
var 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['object_start']) +
pageOffset * fragmentation['unit_size_bytes'],
size * fragmentation['unit_size_bytes']);
}
void _handleMouseMove(MouseEvent event) {
var info = _objectAt(event.offset);
var addressString = '${info.size}B @ 0x${info.address.toRadixString(16)}';
var className = _classNameAt(event.offset);
status = (className == '') ? '-' : '$className $addressString';
}
void _handleClick(MouseEvent event) {
var address = _objectAt(event.offset).address.toRadixString(16);
window.location.hash = "/${fragmentation.isolate.link}/address/$address";
}
void _updateFragmentationData() {
if (fragmentation == null || _fragmentationCanvas == null) {
return;
}
_updateClassList(
fragmentation['class_list'], fragmentation['free_class_id']);
var pages = fragmentation['pages'];
var width = _fragmentationCanvas.parent.client.width;
_pageHeight = _PAGE_SEPARATION_HEIGHT +
fragmentation['page_size_bytes'] ~/
fragmentation['unit_size_bytes'] ~/ width;
var height = _pageHeight * pages.length;
_fragmentationData =
_fragmentationCanvas.context2D.createImageData(width, height);
_fragmentationCanvas.width = _fragmentationData.width;
_fragmentationCanvas.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';
if (startPage >= pages.length) {
return;
}
var startY = startPage * _pageHeight;
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();
}
}
var endY = startY + _pageHeight;
while (pixel.point.y < endY) {
pixel.color = _pageSeparationColor;
pixel = pixel.next();
}
_fragmentationCanvas.context2D.putImageData(
_fragmentationData, 0, 0, 0, startY, _fragmentationData.width, endY);
// Continue with the next page, asynchronously.
new Future(() {
_renderPages(startPage + 1);
});
}
void refresh(var done) {
if (fragmentation == null) {
return;
}
fragmentation.isolate.get('heapmap').then((ServiceMap response) {
assert(response['type'] == 'HeapMap');
fragmentation = response;
}).catchError((e, st) {
Logger.root.info('$e $st');
}).whenComplete(done);
}
void fragmentationChanged(oldValue) {
// Async, in case enteredView has not yet run (observed in JS version).
new Future(() {
_updateFragmentationData();
});
}
}