Replace heatmap with treemap in memory page
diff --git a/packages/devtools_app/lib/src/charts/treemap.dart b/packages/devtools_app/lib/src/charts/treemap.dart
new file mode 100644
index 0000000..a093093
--- /dev/null
+++ b/packages/devtools_app/lib/src/charts/treemap.dart
@@ -0,0 +1,546 @@
+import 'package:flutter/material.dart';
+
+import '../common_widgets.dart';
+import '../trees.dart';
+import '../ui/colors.dart';
+import '../utils.dart';
+
+enum PivotType { pivotByMiddle, pivotBySize }
+
+class Treemap extends StatelessWidget {
+ const Treemap.fromRoot({
+ @required this.rootNode,
+ this.nodes,
+ @required this.levelsVisible,
+ @required this.isOutermostLevel,
+ @required this.height,
+ @required this.onRootChangedCallback,
+ });
+
+ const Treemap.fromNodes({
+ this.rootNode,
+ @required this.nodes,
+ @required this.levelsVisible,
+ @required this.isOutermostLevel,
+ @required this.height,
+ @required this.onRootChangedCallback,
+ });
+
+ final TreemapNode rootNode;
+
+ final List<TreemapNode> nodes;
+
+ /// The depth of children visible from this Treemap widget.
+ ///
+ /// A decremented level should be passed in when constructing [Treemap.fromRoot],
+ /// but not when constructing [Treemap.fromNodes]. This is because
+ /// when constructing from a root, [Treemap] either builds a nested [Treemap] to
+ /// show its node's children, or it shows its node. When constructing from a list
+ /// of nodes, however, [Treemap] is built to become part of a bigger treemap,
+ /// which means the level should not change.
+ ///
+ /// For example, levelsVisible = 2:
+ /// ```
+ /// _______________
+ /// | Root |
+ /// ---------------
+ /// | 1 |
+ /// | --------- |
+ /// | | 2 | |
+ /// | | | |
+ /// | | | |
+ /// | --------- |
+ /// ---------------
+ /// ```
+ final int levelsVisible;
+
+ /// Whether current levelsVisible matches the outermost level.
+ final bool isOutermostLevel;
+
+ final double height;
+
+ final void Function(TreemapNode node) onRootChangedCallback;
+
+ static const PivotType pivotType = PivotType.pivotBySize;
+
+ static const treeMapHeaderHeight = 20.0;
+
+ static const minHeightToDisplayTitleText = 20.0;
+
+ static const minHeightToDisplayCellText = 40.0;
+
+ /// Computes the total size of a given list of treemap nodes.
+ /// [endIndex] defaults to nodes.length - 1.
+ int computeByteSizeForNodes({
+ @required List<TreemapNode> nodes,
+ int startIndex = 0,
+ int endIndex,
+ }) {
+ endIndex ??= nodes.length - 1;
+ int sum = 0;
+ for (int i = startIndex; i <= endIndex; i++) {
+ sum += nodes[i].byteSize;
+ }
+ return sum;
+ }
+
+ int computePivot(List<TreemapNode> children) {
+ switch (pivotType) {
+ case PivotType.pivotByMiddle:
+ return (children.length / 2).floor();
+ case PivotType.pivotBySize:
+ int pivotIndex = -1;
+ double maxSize = double.negativeInfinity;
+ for (int i = 0; i < children.length; i++) {
+ if (children[i].byteSize > maxSize) {
+ maxSize = children[i].byteSize.toDouble();
+ pivotIndex = i;
+ }
+ }
+ return pivotIndex;
+ default:
+ return -1;
+ }
+ }
+
+ /// Implements the ordered treemap algorithm studied in [this research paper](https://www.cs.umd.edu/~ben/papers/Shneiderman2001Ordered.pdf).
+ ///
+ /// **Algorithm**
+ ///
+ /// Divides a given list of treemap nodes into four parts:
+ /// L1, P, L2, L3.
+ ///
+ /// P (pivot) is the treemap node chosen to be the pivot based on the pivot type.
+ /// L1 includes all treemap nodes before the pivot treemap node.
+ /// L2 and L3 combined include all treemap nodes after the pivot treemap node.
+ /// A combination of elements are put into L2 and L3 so that
+ /// the aspect ratio of the pivot cell (P) is as close to 1 as it can be.
+ ///
+ /// **Layout**
+ /// ```
+ /// ----------------------
+ /// | | P | |
+ /// | | | |
+ /// | L1 |------| L3 |
+ /// | | L2 | |
+ /// | | | |
+ /// ----------------------
+ /// ```
+ List<Positioned> buildTreemaps({
+ @required List<TreemapNode> children,
+ @required double width,
+ @required double height,
+ }) {
+ final isHorizontalRectangle = width > height;
+
+ final totalByteSize = computeByteSizeForNodes(nodes: children);
+ if (children.isEmpty) {
+ return [];
+ }
+
+ // Sort the list of treemap nodes, descending in size.
+ children.sort((a, b) => b.byteSize.compareTo(a.byteSize));
+ if (children.length <= 2) {
+ final positionedChildren = <Positioned>[];
+ double offset = 0;
+
+ for (final child in children) {
+ final ratio = child.byteSize / totalByteSize;
+
+ positionedChildren.add(
+ Positioned(
+ left: isHorizontalRectangle ? offset : 0.0,
+ top: isHorizontalRectangle ? 0.0 : offset,
+ width: isHorizontalRectangle ? ratio * width : width,
+ height: isHorizontalRectangle ? height : ratio * height,
+ child: Treemap.fromRoot(
+ rootNode: child,
+ levelsVisible: levelsVisible - 1,
+ isOutermostLevel: false,
+ onRootChangedCallback: onRootChangedCallback,
+ height: height,
+ ),
+ ),
+ );
+ offset += isHorizontalRectangle ? ratio * width : ratio * height;
+ }
+
+ return positionedChildren;
+ }
+
+ final pivotIndex = computePivot(children);
+
+ final pivotNode = children[pivotIndex];
+ final pivotByteSize = pivotNode.byteSize;
+
+ final list1 = children.sublist(0, pivotIndex);
+ final list1ByteSize = computeByteSizeForNodes(nodes: list1);
+
+ var list2 = <TreemapNode>[];
+ int list2ByteSize = 0;
+ var list3 = <TreemapNode>[];
+ int list3ByteSize = 0;
+
+ // The maximum amount of data we can put in [list3].
+ final l3MaxLength = children.length - pivotIndex - 1;
+ int bestIndex = 0;
+ double pivotBestWidth = 0;
+ double pivotBestHeight = 0;
+
+ // We need to be able to put at least 3 elements in [list3] for this algorithm.
+ if (l3MaxLength >= 3) {
+ double pivotBestAspectRatio = double.infinity;
+ // Iterate through different combinations of [list2] and [list3] to find
+ // the combination where the aspect ratio of pivot is the lowest.
+ for (int i = pivotIndex + 1; i < children.length; i++) {
+ final list2Size = computeByteSizeForNodes(
+ nodes: children,
+ startIndex: pivotIndex + 1,
+ endIndex: i,
+ );
+
+ // Calculate the aspect ratio for the pivot treemap node.
+ final pivotAndList2Ratio = (pivotByteSize + list2Size) / totalByteSize;
+ final pivotRatio = pivotByteSize / (pivotByteSize + list2Size);
+
+ final pivotWidth = isHorizontalRectangle
+ ? pivotAndList2Ratio * width
+ : pivotRatio * width;
+
+ final pivotHeight = isHorizontalRectangle
+ ? pivotRatio * height
+ : pivotAndList2Ratio * height;
+
+ final pivotAspectRatio = pivotWidth / pivotHeight;
+
+ // Best aspect ratio that is the closest to 1.
+ if ((1 - pivotAspectRatio).abs() < (1 - pivotBestAspectRatio).abs()) {
+ pivotBestAspectRatio = pivotAspectRatio;
+ bestIndex = i;
+ // Kept track of width and height to construct the pivot cell.
+ pivotBestWidth = pivotWidth;
+ pivotBestHeight = pivotHeight;
+ }
+ }
+ // Split the rest of the data into [list2] and [list3].
+ list2 = children.sublist(pivotIndex + 1, bestIndex + 1);
+ list2ByteSize = computeByteSizeForNodes(nodes: list2);
+
+ list3 = children.sublist(bestIndex + 1);
+ list3ByteSize = computeByteSizeForNodes(nodes: list3);
+ } else {
+ // Put all data in [list2] and none in [list3].
+ list2 = children.sublist(pivotIndex + 1);
+ list2ByteSize = computeByteSizeForNodes(nodes: list2);
+
+ final pivotAndList2Ratio =
+ (pivotByteSize + list2ByteSize) / totalByteSize;
+ final pivotRatio = pivotByteSize / (pivotByteSize + list2ByteSize);
+ pivotBestWidth = isHorizontalRectangle
+ ? pivotAndList2Ratio * width
+ : pivotRatio * width;
+ pivotBestHeight = isHorizontalRectangle
+ ? pivotRatio * height
+ : pivotAndList2Ratio * height;
+ }
+
+ final positionedTreemaps = <Positioned>[];
+
+ // Contruct list 1 sub-treemap.
+ final list1SizeRatio = list1ByteSize / totalByteSize;
+ final list1Width = isHorizontalRectangle ? width * list1SizeRatio : width;
+ final list1Height =
+ isHorizontalRectangle ? height : height * list1SizeRatio;
+ if (list1.isNotEmpty) {
+ positionedTreemaps.add(
+ Positioned(
+ left: 0.0,
+ right: 0.0,
+ width: list1Width,
+ height: list1Height,
+ child: Treemap.fromNodes(
+ nodes: list1,
+ levelsVisible: levelsVisible,
+ isOutermostLevel: isOutermostLevel,
+ onRootChangedCallback: onRootChangedCallback,
+ height: height,
+ ),
+ ),
+ );
+ }
+
+ // Construct list 2 sub-treemap.
+ final list2Width =
+ isHorizontalRectangle ? pivotBestWidth : width - pivotBestWidth;
+ final list2Height =
+ isHorizontalRectangle ? height - pivotBestHeight : pivotBestHeight;
+ final list2XCoord = isHorizontalRectangle ? list1Width : 0.0;
+ final list2YCoord = isHorizontalRectangle ? pivotBestHeight : list1Height;
+ if (list2.isNotEmpty) {
+ positionedTreemaps.add(
+ Positioned(
+ left: list2XCoord,
+ top: list2YCoord,
+ width: list2Width,
+ height: list2Height,
+ child: Treemap.fromNodes(
+ nodes: list2,
+ levelsVisible: levelsVisible,
+ isOutermostLevel: isOutermostLevel,
+ onRootChangedCallback: onRootChangedCallback,
+ height: height,
+ ),
+ ),
+ );
+ }
+
+ // Construct pivot cell.
+ final pivotXCoord = isHorizontalRectangle ? list1Width : list2Width;
+ final pivotYCoord = isHorizontalRectangle ? 0.0 : list1Height;
+
+ positionedTreemaps.add(
+ Positioned(
+ left: pivotXCoord,
+ top: pivotYCoord,
+ width: pivotBestWidth,
+ height: pivotBestHeight,
+ child: Treemap.fromRoot(
+ rootNode: pivotNode,
+ levelsVisible: levelsVisible - 1,
+ isOutermostLevel: false,
+ onRootChangedCallback: onRootChangedCallback,
+ height: height,
+ ),
+ ),
+ );
+
+ // Construct list 3 sub-treemap.
+ final list3Ratio = list3ByteSize / totalByteSize;
+ final list3Width = isHorizontalRectangle ? list3Ratio * width : width;
+ final list3Height = isHorizontalRectangle ? height : list3Ratio * height;
+ final list3XCoord =
+ isHorizontalRectangle ? list1Width + pivotBestWidth : 0.0;
+ final list3YCoord =
+ isHorizontalRectangle ? 0.0 : list1Height + pivotBestHeight;
+
+ if (list3.isNotEmpty) {
+ positionedTreemaps.add(
+ Positioned(
+ left: list3XCoord,
+ top: list3YCoord,
+ width: list3Width,
+ height: list3Height,
+ child: Treemap.fromNodes(
+ nodes: list3,
+ levelsVisible: levelsVisible,
+ isOutermostLevel: isOutermostLevel,
+ onRootChangedCallback: onRootChangedCallback,
+ height: height,
+ ),
+ ),
+ );
+ }
+
+ return positionedTreemaps;
+ }
+
+ Text buildNameAndSizeText({
+ @required Color fontColor,
+ @required bool oneLine,
+ }) {
+ return Text(
+ rootNode.displayText(oneLine: oneLine),
+ style: TextStyle(color: fontColor),
+ textAlign: TextAlign.center,
+ overflow: TextOverflow.ellipsis,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (rootNode == null && nodes.isNotEmpty) {
+ return buildSubTreemaps();
+ } else {
+ return buildTreemap(context);
+ }
+ }
+
+ /// **Treemap widget layout**
+ /// ```
+ /// ----------------------------
+ /// | Title Text |
+ /// |--------------------------|
+ /// | |
+ /// | Cell |
+ /// | |
+ /// | |
+ /// ----------------------------
+ /// ```
+ Widget buildTreemap(BuildContext context) {
+ if (levelsVisible > 0 && rootNode.children.isNotEmpty) {
+ return Padding(
+ padding: const EdgeInsets.all(1.0),
+ child: Column(
+ children: [
+ if (height > minHeightToDisplayTitleText) buildTitleText(context),
+ Expanded(
+ child: Treemap.fromNodes(
+ nodes: rootNode.children,
+ levelsVisible: levelsVisible,
+ isOutermostLevel: isOutermostLevel,
+ onRootChangedCallback: onRootChangedCallback,
+ height: height,
+ ),
+ ),
+ ],
+ ),
+ );
+ } else {
+ return Column(
+ children: [
+ Expanded(
+ child: buildSelectable(
+ child: Container(
+ decoration: BoxDecoration(
+ color: mainUiColor,
+ border: Border.all(color: Colors.black87),
+ ),
+ child: Center(
+ child: height > minHeightToDisplayCellText
+ ? buildNameAndSizeText(
+ fontColor: Colors.black,
+ oneLine: false,
+ )
+ : const SizedBox(),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+ }
+
+ Widget buildTitleText(BuildContext context) {
+ if (isOutermostLevel) {
+ final pathFromRoot = rootNode.pathFromRoot();
+ // Build breadcrumbs navigator.
+ return Container(
+ height: treeMapHeaderHeight,
+ child: ListView.separated(
+ shrinkWrap: true,
+ scrollDirection: Axis.horizontal,
+ separatorBuilder: (context, index) {
+ return const Text(' > ');
+ },
+ itemCount: pathFromRoot.length,
+ itemBuilder: (BuildContext context, int index) {
+ return buildSelectable(
+ child: Text(
+ index < pathFromRoot.length - 1
+ ? pathFromRoot[index].name
+ : pathFromRoot[index].displayText(),
+ ),
+ newRoot: pathFromRoot[index],
+ );
+ },
+ ),
+ );
+ } else {
+ return buildSelectable(
+ child: Container(
+ height: treeMapHeaderHeight,
+ width: double.infinity,
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.black87),
+ ),
+ child: buildNameAndSizeText(
+ fontColor: Theme.of(context).textTheme.bodyText2.color,
+ oneLine: true,
+ ),
+ ),
+ );
+ }
+ }
+
+ /// Builds a selectable container with [child] as its child.
+ ///
+ /// Selecting this widget will trigger a re-root of the tree
+ /// to the associated [TreemapNode].
+ ///
+ /// The default value for newRoot is [rootNode].
+ Tooltip buildSelectable({@required Widget child, TreemapNode newRoot}) {
+ newRoot ??= rootNode;
+ return Tooltip(
+ message: rootNode.displayText(),
+ waitDuration: tooltipWait,
+ preferBelow: false,
+ child: InkWell(
+ onTap: () {
+ if (rootNode.children.isNotEmpty) onRootChangedCallback(newRoot);
+ },
+ child: child,
+ ),
+ );
+ }
+
+ Widget buildSubTreemaps() {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Stack(
+ children: buildTreemaps(
+ children: nodes,
+ width: constraints.maxWidth,
+ height: constraints.maxHeight,
+ ),
+ );
+ },
+ );
+ }
+}
+
+class TreemapNode extends TreeNode<TreemapNode> {
+ TreemapNode({
+ @required this.name,
+ this.byteSize = 0,
+ this.childrenMap = const <String, TreemapNode>{},
+ }) : assert(name != null),
+ assert(byteSize != null),
+ assert(childrenMap != null);
+
+ final String name;
+ final Map<String, TreemapNode> childrenMap;
+ int byteSize;
+
+ String displayText({bool oneLine = true}) {
+ final separator = oneLine ? ' ' : '\n';
+ return '$name$separator[${prettyPrintBytes(byteSize, includeUnit: true)}]';
+ }
+
+ /// Returns a list of [TreemapNode] in the path from root node to [this].
+ List<TreemapNode> pathFromRoot() {
+ TreemapNode node = this;
+ final path = <TreemapNode>[];
+ while (node != null) {
+ path.add(node);
+ node = node.parent;
+ }
+ return path.reversed.toList();
+ }
+
+ void printTree() {
+ printTreeHelper(this, '');
+ }
+
+ void printTreeHelper(TreemapNode root, String tabs) {
+ print(tabs + '$root');
+ for (final child in root.children) {
+ printTreeHelper(child, tabs + '\t');
+ }
+ }
+
+ @override
+ String toString() {
+ return '{name: $name, size: $byteSize}';
+ }
+}
diff --git a/packages/devtools_app/lib/src/logging/logging_controller.dart b/packages/devtools_app/lib/src/logging/logging_controller.dart
index 8960c04..eb01c98 100644
--- a/packages/devtools_app/lib/src/logging/logging_controller.dart
+++ b/packages/devtools_app/lib/src/logging/logging_controller.dart
@@ -386,7 +386,7 @@
final String summary = '${isolateRef['name']} • '
'${e.json['reason']} collection in $time ms • '
- '${printMb(usedBytes)} MB used of ${printMb(capacityBytes)} MB';
+ '${printMB(usedBytes, includeUnit: true)} used of ${printMB(capacityBytes, includeUnit: true)}';
final Map<String, dynamic> event = <String, dynamic>{
'reason': e.json['reason'],
diff --git a/packages/devtools_app/lib/src/memory/memory_controller.dart b/packages/devtools_app/lib/src/memory/memory_controller.dart
index 5687d7f..77081ff 100644
--- a/packages/devtools_app/lib/src/memory/memory_controller.dart
+++ b/packages/devtools_app/lib/src/memory/memory_controller.dart
@@ -52,12 +52,12 @@
static const logFilenamePrefix = 'memory_log_';
- final _showHeatMap = ValueNotifier<bool>(false);
+ final _showTreemap = ValueNotifier<bool>(false);
- ValueListenable<bool> get showHeatMap => _showHeatMap;
+ ValueListenable<bool> get showTreemap => _showTreemap;
- void toggleShowHeatMap(bool value) {
- _showHeatMap.value = value;
+ void toggleShowTreeMap(bool value) {
+ _showTreemap.value = value;
}
final snapshots = <Snapshot>[];
diff --git a/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart b/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart
index 70cf4f7..28f40a3 100644
--- a/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart
+++ b/packages/devtools_app/lib/src/memory/memory_heap_tree_view.dart
@@ -23,8 +23,8 @@
import 'memory_controller.dart';
import 'memory_filter.dart';
import 'memory_graph_model.dart';
-import 'memory_heatmap.dart';
import 'memory_snapshot_models.dart';
+import 'memory_treemap.dart';
import 'memory_utils.dart';
class HeapTree extends StatefulWidget {
@@ -227,12 +227,8 @@
: _isSnapshotComplete ? 'Done' : '...'),
]);
} else if (controller.snapshotByLibraryData != null) {
- if (controller.showHeatMap.value) {
- snapshotDisplay = HeatMapSizeAnalyzer(
- child: SizedBox.expand(
- child: FlameChart(controller),
- ),
- );
+ if (controller.showTreemap.value) {
+ snapshotDisplay = MemoryTreemap(controller);
} else {
snapshotDisplay = MemorySnapshotTable();
}
@@ -268,7 +264,7 @@
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(child: snapshotDisplay),
- if (hasDetails) const SizedBox(width: defaultSpacing),
+ hasDetails ? const SizedBox(width: defaultSpacing) : const SizedBox(),
// TODO(terry): Need better focus handling between 2 tables & up/down
// arrows in the right-side field instance view table.
controller.isLeafSelected
@@ -343,13 +339,13 @@
const SizedBox(width: defaultSpacing),
Row(
children: [
- const Text('Heat Map'),
+ const Text('Treemap'),
Switch(
- value: controller.showHeatMap.value,
+ value: controller.showTreemap.value,
onChanged: (value) {
setState(() {
closeAutoCompleteOverlay();
- controller.toggleShowHeatMap(value);
+ controller.toggleShowTreeMap(value);
controller.search = '';
controller.selectedLeaf = null;
});
@@ -358,13 +354,13 @@
],
),
const SizedBox(width: defaultSpacing),
- controller.showHeatMap.value
+ controller.showTreemap.value
? const SizedBox()
: _groupByDropdown(textTheme),
const SizedBox(width: defaultSpacing),
// TODO(terry): Mechanism to handle expand/collapse on both
// tables objects/fields. Maybe notion in table?
- controller.showHeatMap.value
+ controller.showTreemap.value
? const SizedBox()
: OutlineButton(
key: collapseAllButtonKey,
@@ -386,7 +382,7 @@
: null,
child: const Text('Collapse All'),
),
- controller.showHeatMap.value
+ controller.showTreemap.value
? const SizedBox()
: OutlineButton(
key: expandAllButtonKey,
@@ -463,7 +459,7 @@
),
);
- if (controller.showHeatMap.value && controller.snapshots.isNotEmpty) {
+ if (controller.showTreemap.value && controller.snapshots.isNotEmpty) {
searchFieldFocusNode.requestFocus();
}
diff --git a/packages/devtools_app/lib/src/memory/memory_heatmap.dart b/packages/devtools_app/lib/src/memory/memory_heatmap.dart
deleted file mode 100644
index 5d18241..0000000
--- a/packages/devtools_app/lib/src/memory/memory_heatmap.dart
+++ /dev/null
@@ -1,684 +0,0 @@
-// Copyright 2020 The Chromium Authors. 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:math' as math;
-import 'dart:math';
-import 'dart:ui';
-
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart' hide TextStyle;
-import 'package:flutter/rendering.dart' hide TextStyle;
-import 'package:flutter/widgets.dart' hide TextStyle;
-import 'package:intl/intl.dart';
-import 'package:provider/provider.dart';
-
-import '../auto_dispose_mixin.dart';
-import '../ui/colors.dart';
-
-import 'memory_controller.dart';
-import 'memory_graph_model.dart';
-import 'memory_utils.dart';
-
-/// UX navigation in a heat map e.g.,
-/// ------------------------------------------------------------
-/// | ||| || |||| | | new String | | || Foundation (38M) |
-/// ------------------------------------------------------------
-/// || | | | | List | String | | || src (42M) |
-/// ------------------------------------------------------------
-/// | src (36.4M) | dart:core (58M) |package:flutter (42M)|
-/// ------------------------------------------------------------
-/// | Root (136M) |
-/// ------------------------------------------------------------
-/// Mouse action (click or mouse scroll wheel) over a rectangle for example
-/// package:flutter will to expand its contained children to more rectangles
-/// upwards. The sizes of the contained children (if an aggregated parent).
-/// Mouse action over, upward, children will further expand its children in
-/// a upward fashion too. To collapse a rectangle move the mouse below a child
-/// rectangle (its parent) and click or mouse scroll.
-
-class HeatMapSizeAnalyzer extends SingleChildRenderObjectWidget {
- const HeatMapSizeAnalyzer({
- Key key,
- Widget child,
- }) : super(key: key, child: child);
-
- @override
- RenderFlameChart createRenderObject(BuildContext context) {
- return RenderFlameChart();
- }
-}
-
-class RenderFlameChart extends RenderProxyBox {
- @override
- void paint(PaintingContext context, Offset offset) {
- if (child != null) {
- context.paintChild(child, offset);
- }
- }
-}
-
-class FlameChart extends StatefulWidget {
- const FlameChart(
- this.controller,
- );
-
- final MemoryController controller;
-
- @override
- FlameChartState createState() => FlameChartState(controller);
-}
-
-class FlameChartState extends State<FlameChart> with AutoDisposeMixin {
- FlameChartState(this.controller);
-
- InstructionsSize sizes;
-
- Map<String, Function> callbacks = {};
-
- MemoryController controller;
-
- Widget snapshotDisplay;
-
- _FlameChart _flameChart;
-
- @override
- void initState() {
- super.initState();
- }
-
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
-
- // TODO(terry): Unable to short-circuit need to investigate why?
- controller = Provider.of<MemoryController>(context);
-
- cancel();
-
- addAutoDisposeListener(controller.selectedSnapshotNotifier, () {
- setState(() {
- controller.computeAllLibraries(true, true);
-
- sizes = InstructionsSize.fromSnapshop(controller);
- });
- });
-
- addAutoDisposeListener(controller.filterNotifier, () {
- setState(() {
- controller.computeAllLibraries(true, true);
- });
- });
-
- addAutoDisposeListener(controller.selectTheSearchNotifier, () {
- setState(() {
- if (_trySelectItem()) {
- closeAutoCompleteOverlay();
- }
- });
- });
-
- addAutoDisposeListener(controller.searchNotifier, () {
- setState(() {
- if (_trySelectItem()) {
- closeAutoCompleteOverlay();
- }
- });
- });
-
- addAutoDisposeListener(controller.searchAutoCompleteNotifier, () {
- setState(autoCompleteOverlaySetState(controller, context));
- });
- }
-
- /// Returns true if node found and node selected.
- bool _trySelectItem() {
- if (controller.search.isNotEmpty) {
- if (controller.selectTheSearch) {
- // Select the node.
- final node = findNode(controller.search);
- selectNode(node);
-
- closeAutoCompleteOverlay();
-
- controller.selectTheSearch = false;
- controller.search = '';
- return true;
- } else {
- var matches = matchNames(controller.search)..sort();
- // No exact match, return top matches.
- matches = matches.sublist(0, min(topMatchesLimit, matches.length));
- controller.searchAutoComplete.value = matches;
- }
- } else if (controller.selectTheSearch) {
- // Escape hit, on empty search.
- selectNode(null);
- controller.selectTheSearch = false;
- }
-
- return false;
- }
-
- @override
- void dispose() {
- super.dispose();
- }
-
- @override
- Widget build(BuildContext context) {
- sizes = InstructionsSize.fromSnapshop(controller);
-
- if (sizes != null) {
- _flameChart = _FlameChart(
- sizes,
- // TODO(terry): Can the color range match flame chart? Review with UX.
- memoryHeatMapLightColor,
- memoryHeatMapDarkColor,
- callbacks,
- );
- return _flameChart;
- } else {
- return const SizedBox();
- }
- }
-
- List<String> matchNames(String searchValue) {
- final MatchNamesFunction callback = _flameChart.callbacks[matchNamesKey];
- return callback(searchValue);
- }
-
- Node findNode(String searchValue) {
- final FindNodeFunction callback = _flameChart.callbacks[findNodeKey];
- return callback(searchValue);
- }
-
- void selectNode(Node nodeValue) {
- final SelectNodeFunction callback = _flameChart.callbacks[selectNodeKey];
- callback(nodeValue);
- }
-}
-
-/// Definitions of exposed callback methods stored in callback Map the key
-/// is the function name (String) and the value a callback function signature.
-
-/// matchNames callback name.
-const matchNamesKey = 'matchNames';
-
-/// matchNames callback signature.
-typedef MatchNamesFunction = List<String> Function(String);
-
-/// findNode callback name.
-const findNodeKey = 'findNode';
-
-/// findNode callback signature.
-typedef FindNodeFunction = Node Function(String);
-
-/// selectNode callback name.
-const selectNodeKey = 'selectNode';
-
-/// selectNode callback signature.
-typedef SelectNodeFunction = void Function(Node);
-
-class _FlameChart extends LeafRenderObjectWidget {
- const _FlameChart(
- this.sizes, this.lightColor, this.darkColor, this.callbacks);
-
- final InstructionsSize sizes;
-
- final Color lightColor;
-
- final Color darkColor;
-
- final Map<String, Function> callbacks;
-
- @override
- FlameChartRenderObject createRenderObject(BuildContext context) {
- return FlameChartRenderObject()
- // callbacks must be before sizes as hookup is done in sizes setter.
- ..callbacks = callbacks
- ..sizes = sizes
- ..lightColor = lightColor
- ..darkColor = darkColor;
- }
-
- @override
- void updateRenderObject(
- BuildContext context,
- FlameChartRenderObject renderObject,
- ) {
- renderObject
- ..sizes = sizes
- ..lightColor = lightColor
- ..darkColor = darkColor;
- }
-}
-
-class FlameChartRenderObject extends RenderBox {
- FlameChartRenderObject();
-
- InstructionsSize _sizes;
-
- Offset paintOffset;
-
- Map<String, Function> callbacks;
-
- set sizes(InstructionsSize value) {
- callbacks[matchNamesKey] = _exposeMatchNames;
- callbacks[findNodeKey] = _exposeFindNode;
- callbacks[selectNodeKey] = _exposeSelectNode;
-
- if (value == _sizes) {
- return;
- }
- _sizes = value;
- _selectedNode ??= value.root;
- markNeedsPaint();
- }
-
- Color _lightColor;
-
- set lightColor(Color value) {
- if (value == _lightColor) {
- return;
- }
- _lightColor = value;
- markNeedsPaint();
- }
-
- Color _darkColor;
-
- set darkColor(Color value) {
- if (value == _lightColor) {
- return;
- }
- _darkColor = value;
- markNeedsPaint();
- }
-
- List<String> _exposeMatchNames(String searchName) {
- return matchNodeNames(_sizes.root.children, searchName);
- }
-
- /// Look for the node with a particular name (depth first traversal).
- List<String> matchNodeNames(Map<String, Node> children, String searchName) {
- final matches = <String>[];
- final matchName = searchName.toLowerCase();
- for (var child in children.entries) {
- final node = child.value;
-
- final lcNodeName = node.name.toLowerCase();
- if (!lcNodeName.endsWith('.dart') && lcNodeName.startsWith(matchName)) {
- matches.add(node.name);
- }
- if (node.children.isNotEmpty) {
- final childMatches = matchNodeNames(node.children, searchName);
- if (childMatches.isNotEmpty) {
- matches.addAll(childMatches);
- }
- }
- }
-
- return matches;
- }
-
- Node _exposeFindNode(String searchName) {
- return findNode(_sizes.root.children, searchName);
- }
-
- /// Look for the node with a particular name (depth first traversal).
- Node findNode(Map<String, Node> children, String searchName) {
- final matchName = searchName.toLowerCase();
- for (var child in children.entries) {
- final node = child.value;
- if (node.children.isEmpty) {
- return node.name.toLowerCase() == matchName ? node : null;
- } else {
- if (node.name.toLowerCase() == matchName) return node;
- final foundNode = findNode(node.children, matchName);
- if (foundNode != null) return foundNode;
- }
- }
-
- return null;
- }
-
- void _exposeSelectNode(Node value) {
- selectedNode = value;
- }
-
- Node _selectedNode;
-
- set selectedNode(Node value) {
- value ??= _sizes.root;
- if (value == _selectedNode) {
- return;
- }
- _selectedNode = value;
-
- markNeedsPaint();
- }
-
- @override
- bool get sizedByParent => true;
-
- @override
- bool hitTest(BoxHitTestResult result, {Offset position}) {
- final computedPosition =
- Offset(position.dx + paintOffset.dx, position.dy + paintOffset.dy);
-
- final node = _selectedNode.findRect(computedPosition);
- if (node != null) {
- selectedNode = node;
- }
-
- return super.hitTest(result, position: position);
- }
-
- @override
- void paint(PaintingContext context, Offset offset) {
- paintOffset = offset;
-
- final rootWidth = size.width;
- final top = _paintAncestors(context, _selectedNode.ancestors);
-
- _paintNode(
- context, _selectedNode, 0 + paintOffset.dx, rootWidth, top - offset.dy);
-
- _paintChildren(
- context: context,
- currentLeft: 1 + offset.dx,
- parentSize: _selectedNode.byteSize,
- children: _selectedNode.children.values,
- topFactor: top + Node.nodeHeight + 1 - offset.dy,
- maxWidth: rootWidth - 1,
- );
- }
-
- final _logs = <int, double>{};
- final _inverseColors = <Color, Color>{};
-
- void _paintNode(
- PaintingContext context,
- Node node,
- double left,
- double width,
- double top,
- ) {
- node.rect = Rect.fromLTWH(left, size.height - top, width, Node.nodeHeight);
- final double t =
- _logs.putIfAbsent(node.byteSize, () => math.log(node.byteSize)) /
- _logs.putIfAbsent(
- _sizes.root.byteSize, () => math.log(_sizes.root.byteSize));
- final Color backgroundColor = Color.lerp(
- _lightColor,
- _darkColor,
- t,
- );
-
- context.canvas.drawRRect(
- RRect.fromRectAndRadius(node.rect, const Radius.circular(2.0)),
- Paint()..color = backgroundColor,
- );
- // Don't bother figuring out the text length if the box is too small.
- if (width < 100) {
- return;
- }
-
- final formattedByteSize = NumberFormat.compact().format(node.byteSize);
-
- final builder = ParagraphBuilder(
- ParagraphStyle(
- textAlign: TextAlign.center,
- maxLines: 2,
- ellipsis: '...',
- ),
- )
- ..pushStyle(
- TextStyle(
- color: _inverseColors.putIfAbsent(backgroundColor, () {
- return HSLColor.fromColor(backgroundColor).lightness > .7
- ? const Color(0xFF000000)
- : const Color(0xFFFFFFFF);
- }),
- fontFamily: 'Courier New',
- ),
- )
- ..addText('${node.name}\n($formattedByteSize)');
- final Paragraph paragraph = builder.build()
- ..layout(ParagraphConstraints(width: width - 5));
- context.canvas.drawParagraph(
- paragraph,
- Offset(10 + left, size.height - top + 10),
- );
- }
-
- double _paintAncestors(PaintingContext context, List<Node> nodes) {
- double top = Node.nodeHeight;
- for (var node in nodes.reversed) {
- _paintNode(
- context, node, 0 + paintOffset.dx, size.width, top - paintOffset.dy);
- top += Node.nodeHeight;
- }
- return top;
- }
-
- void _paintChildren({
- PaintingContext context,
- double currentLeft,
- int parentSize,
- Iterable<Node> children,
- double topFactor,
- double maxWidth,
- }) {
- double left = currentLeft;
-
- for (var child in children) {
- final double width = child.byteSize / parentSize * maxWidth;
- _paintNode(context, child, left, width, topFactor);
-
- if (!child.isLeaf) {
- final double factor = math.max(
- 0.0001,
- math.min(
- math.log(maxWidth * .01),
- math.log(width * .1),
- ),
- );
- // Very minor chunk of memory, just skip display for now. Smaller than 1 pixel in width.
- if (factor < width) {
- _paintChildren(
- context: context,
- currentLeft: left + factor,
- parentSize: child.byteSize,
- children: child.children.values,
- topFactor: topFactor + Node.nodeHeight + 1,
- maxWidth: width - (2 * factor),
- );
- }
- }
- left += width;
- }
- }
-}
-
-class InstructionsSize {
- const InstructionsSize(this.root);
-
- factory InstructionsSize.fromSnapshop(MemoryController controller) {
- final Map<String, Node> rootChildren = <String, Node>{};
- final Node root = Node(
- 'root',
- children: rootChildren,
- );
- Node currentParent = root;
-
- // TODO(terry): Should heat map be all memory or just the filtered group?
- // Using rawGroup not graph.groupByLibrary.
- controller.heapGraph.rawGroupByLibrary.forEach(
- (libraryGroup, value) {
- final classes = value;
- for (final theClass in classes) {
- final shallowSize = theClass.instancesTotalShallowSizes;
- var className = theClass.name;
- if (shallowSize == 0 ||
- libraryGroup == null ||
- className == null ||
- className == '::') {
- continue;
- }
-
- // Ensure the empty library name is our group name e.g., '' -> 'src'.
- String libraryName = theClass.libraryUri.toString();
- if (libraryName.isEmpty) {
- libraryName = libraryGroup;
- }
-
- // Map class names to familar user names.
- final predefined =
- predefinedClasses[LibraryClass(libraryName, className)];
- if (predefined != null) {
- className = predefined.prettyName;
- }
-
- final symbol = Symbol(
- name: 'new $className',
- size: shallowSize,
- libraryUri: libraryName,
- className: className,
- );
-
- Map<String, Node> currentChildren = rootChildren;
- final Node parentReset = currentParent;
- for (String pathPart in symbol.parts) {
- currentChildren.putIfAbsent(
- pathPart,
- () => Node(pathPart,
- parent: currentParent, children: <String, Node>{}),
- );
- currentChildren[pathPart].byteSize += symbol.size;
- currentParent = currentChildren[pathPart];
- currentChildren = currentChildren[pathPart].children;
- }
- currentParent = parentReset;
- }
- },
- );
-
- root.byteSize = root.children.values
- .fold(0, (int current, Node node) => current + node.byteSize);
-
- final snapshotGraph = controller.snapshots.last.snapshotGraph;
-
- // Add the external heap to the heat map.
- root.children.putIfAbsent('External Heap', () {
- final node = Node(
- 'External Heap',
- parent: root,
- children: <String, Node>{},
- );
- node.byteSize = snapshotGraph.externalSize;
- return node;
- });
-
- // Add the filtered libraries/classes to the heat map.
- root.children.putIfAbsent('All Filtered Libraries', () {
- final node = Node(
- 'All Filtered Libraries',
- parent: root,
- children: <String, Node>{},
- );
- node.byteSize = snapshotGraph.shallowSize - root.byteSize;
- return node;
- });
-
- root.byteSize = snapshotGraph.shallowSize + snapshotGraph.externalSize;
-
- return InstructionsSize(root);
- }
-
- final Node root;
-}
-
-class Node {
- Node(
- this.name, {
- this.byteSize = 0,
- this.parent,
- this.children = const <String, Node>{},
- }) : assert(name != null),
- assert(byteSize != null),
- assert(children != null);
-
- static const double nodeHeight = 50.0;
-
- final String name;
- int byteSize;
- Rect rect = Rect.zero;
- final Node parent;
- final Map<String, Node> children;
-
- Iterable<Node> get ancestors {
- final nodes = <Node>[];
- Node current = this;
- while (current.parent != null) {
- nodes.add(current.parent);
- current = current.parent;
- }
- return nodes;
- }
-
- bool get isLeaf => children.isEmpty;
-
- Node findRect(Offset offset) {
- if (rect.contains(offset)) {
- return this;
- }
- for (var ancestor in ancestors) {
- if (ancestor.rect.contains(offset)) {
- return ancestor;
- }
- }
- for (var child in children.values) {
- final value = child.findRect(offset);
- if (value != null) {
- return value;
- }
- }
- return null;
- }
-
- @override
- String toString() => 'Node($name, $byteSize, $rect)';
-}
-
-class Symbol {
- const Symbol({
- @required this.name,
- @required this.size,
- this.libraryUri,
- this.className,
- }) : assert(name != null),
- assert(size != null);
-
- static Symbol fromMap(Map<String, dynamic> json) {
- return Symbol(
- name: json['n'] as String,
- size: json['s'] as int,
- className: json['c'] as String,
- libraryUri: json['l'] as String,
- );
- }
-
- final String name;
- final int size;
- final String libraryUri;
- final String className;
-
- List<String> get parts {
- return <String>[
- if (libraryUri != null) ...libraryUri.split('/') else '@stubs',
- if (className != null && className.isNotEmpty) className,
- name,
- ];
- }
-}
diff --git a/packages/devtools_app/lib/src/memory/memory_treemap.dart b/packages/devtools_app/lib/src/memory/memory_treemap.dart
new file mode 100644
index 0000000..a810167
--- /dev/null
+++ b/packages/devtools_app/lib/src/memory/memory_treemap.dart
@@ -0,0 +1,266 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart' hide TextStyle;
+import 'package:flutter/widgets.dart' hide TextStyle;
+import 'package:provider/provider.dart';
+
+import '../auto_dispose_mixin.dart';
+import '../charts/treemap.dart';
+
+import 'memory_controller.dart';
+import 'memory_graph_model.dart';
+import 'memory_utils.dart';
+
+class MemoryTreemap extends StatefulWidget {
+ const MemoryTreemap(this.controller);
+
+ final MemoryController controller;
+
+ @override
+ MemoryTreemapState createState() => MemoryTreemapState(controller);
+}
+
+class MemoryTreemapState extends State<MemoryTreemap> with AutoDisposeMixin {
+ MemoryTreemapState(this.controller);
+
+ InstructionsSize sizes;
+
+ Map<String, Function> callbacks = {};
+
+ MemoryController controller;
+
+ Widget snapshotDisplay;
+
+ TreemapNode root;
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+
+ // TODO(terry): Unable to short-circuit need to investigate why?
+ controller = Provider.of<MemoryController>(context);
+
+ sizes = InstructionsSize.fromSnapshot(controller);
+
+ root = sizes.root;
+
+ cancel();
+
+ addAutoDisposeListener(controller.selectedSnapshotNotifier, () {
+ setState(() {
+ controller.computeAllLibraries(true, true);
+
+ sizes = InstructionsSize.fromSnapshot(controller);
+ });
+ });
+
+ addAutoDisposeListener(controller.filterNotifier, () {
+ setState(() {
+ controller.computeAllLibraries(true, true);
+ });
+ });
+ // TODO(peterdjlee): Need to check if applicable to treemap.
+ // addAutoDisposeListener(controller.selectTheSearchNotifier, () {
+ // setState(() {
+ // if (_trySelectItem()) {
+ // closeAutoCompleteOverlay();
+ // }
+ // });
+ // });
+
+ // addAutoDisposeListener(controller.searchNotifier, () {
+ // setState(() {
+ // if (_trySelectItem()) {
+ // closeAutoCompleteOverlay();
+ // }
+ // });
+ // });
+
+ addAutoDisposeListener(controller.searchAutoCompleteNotifier, () {
+ setState(autoCompleteOverlaySetState(controller, context));
+ });
+ }
+
+ void _onRootChanged(TreemapNode newRoot) {
+ setState(() {
+ root = newRoot;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (sizes != null) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return Treemap.fromRoot(
+ rootNode: root,
+ levelsVisible: 2,
+ isOutermostLevel: true,
+ height: constraints.maxHeight,
+ onRootChangedCallback: _onRootChanged,
+ );
+ },
+ );
+ } else {
+ return const SizedBox();
+ }
+ }
+}
+
+/// Definitions of exposed callback methods stored in callback Map the key
+/// is the function name (String) and the value a callback function signature.
+
+/// matchNames callback name.
+const matchNamesKey = 'matchNames';
+
+/// matchNames callback signature.
+typedef MatchNamesFunction = List<String> Function(String);
+
+/// findNode callback name.
+const findNodeKey = 'findNode';
+
+/// findNode callback signature.
+typedef FindNodeFunction = TreemapNode Function(String);
+
+/// selectNode callback name.
+const selectNodeKey = 'selectNode';
+
+/// selectNode callback signature.
+typedef SelectNodeFunction = void Function(TreemapNode);
+
+class InstructionsSize {
+ const InstructionsSize(this.root);
+
+ factory InstructionsSize.fromSnapshot(MemoryController controller) {
+ final rootChildren = <String, TreemapNode>{};
+ final root = TreemapNode(
+ name: 'root',
+ childrenMap: rootChildren,
+ );
+ TreemapNode currentParent = root;
+
+ // TODO(terry): Should treemap be all memory or just the filtered group?
+ // Using rawGroup not graph.groupByLibrary.
+
+ controller.heapGraph.rawGroupByLibrary.forEach(
+ (libraryGroup, value) {
+ final classes = value;
+ for (final theClass in classes) {
+ final shallowSize = theClass.instancesTotalShallowSizes;
+ var className = theClass.name;
+ if (shallowSize == 0 ||
+ libraryGroup == null ||
+ className == null ||
+ className == '::') {
+ continue;
+ }
+
+ // Ensure the empty library name is our group name e.g., '' -> 'src'.
+ String libraryName = theClass.libraryUri.toString();
+ if (libraryName.isEmpty) {
+ libraryName = libraryGroup;
+ }
+
+ // Map class names to familar user names.
+ final predefined =
+ predefinedClasses[LibraryClass(libraryName, className)];
+ if (predefined != null) {
+ className = predefined.prettyName;
+ }
+
+ final symbol = Symbol(
+ name: 'new $className',
+ size: shallowSize,
+ libraryUri: libraryName,
+ className: className,
+ );
+
+ Map<String, TreemapNode> currentChildren = rootChildren;
+ final parentReset = currentParent;
+ for (String pathPart in symbol.parts) {
+ currentChildren.putIfAbsent(
+ pathPart,
+ () {
+ final node = TreemapNode(
+ name: pathPart,
+ childrenMap: <String, TreemapNode>{},
+ );
+ currentParent.addChild(node);
+ return node;
+ },
+ );
+ currentChildren[pathPart].byteSize += symbol.size;
+ currentParent = currentChildren[pathPart];
+ currentChildren = currentChildren[pathPart].childrenMap;
+ }
+ currentParent = parentReset;
+ }
+ },
+ );
+
+ // Get sum of children's sizes.
+ root.byteSize = root.childrenMap.values
+ .fold(0, (int current, TreemapNode node) => current + node.byteSize);
+
+ final snapshotGraph = controller.snapshots.last.snapshotGraph;
+ // Add the external heap to the treemap.
+ root.childrenMap.putIfAbsent('External Heap', () {
+ final node = TreemapNode(
+ name: 'External Heap',
+ childrenMap: <String, TreemapNode>{},
+ )..byteSize = snapshotGraph.externalSize;
+ root.addChild(node);
+ return node;
+ });
+
+ // Add the filtered libraries/classes to the treemap.
+ root.childrenMap.putIfAbsent('All Filtered Libraries', () {
+ final node = TreemapNode(
+ name: 'All Filtered Libraries',
+ childrenMap: <String, TreemapNode>{},
+ )..byteSize = snapshotGraph.shallowSize - root.byteSize;
+ root.addChild(node);
+ return node;
+ });
+
+ root.byteSize = snapshotGraph.shallowSize + snapshotGraph.externalSize;
+
+ return InstructionsSize(root);
+ }
+
+ final TreemapNode root;
+}
+
+class Symbol {
+ const Symbol({
+ @required this.name,
+ @required this.size,
+ this.libraryUri,
+ this.className,
+ }) : assert(name != null),
+ assert(size != null);
+
+ static Symbol fromMap(Map<String, dynamic> json) {
+ return Symbol(
+ name: json['n'] as String,
+ size: json['s'] as int,
+ className: json['c'] as String,
+ libraryUri: json['l'] as String,
+ );
+ }
+
+ final String name;
+ final int size;
+ final String libraryUri;
+ final String className;
+
+ List<String> get parts {
+ return <String>[
+ if (libraryUri != null) ...libraryUri.split('/') else '@stubs',
+ if (className != null && className.isNotEmpty) className,
+ name,
+ ];
+ }
+}
diff --git a/packages/devtools_app/lib/src/ui/colors.dart b/packages/devtools_app/lib/src/ui/colors.dart
index 3096989..3a7e00b 100644
--- a/packages/devtools_app/lib/src/ui/colors.dart
+++ b/packages/devtools_app/lib/src/ui/colors.dart
@@ -13,6 +13,7 @@
Color memoryHeatMapLightColor = const Color(0xFFBBDEFB); // Material BLUE 100
Color memoryHeatMapDarkColor = const Color(0xFF0D47A1); // Material BLUE 900
+// TODO(peterdjlee): Rename mainUiColor to something that more broadly matches where the color is used.
const mainUiColor = Color(0xFF88B1DE);
const mainRasterColor = Color(0xFF2C5DAA);
const mainUnknownColor = Color(0xFFCAB8E9);
diff --git a/packages/devtools_app/lib/src/utils.dart b/packages/devtools_app/lib/src/utils.dart
index 51d86e8..ed39357 100644
--- a/packages/devtools_app/lib/src/utils.dart
+++ b/packages/devtools_app/lib/src/utils.dart
@@ -64,17 +64,34 @@
String percent2(double d) => '${(d * 100).toStringAsFixed(2)}%';
+String prettyPrintBytes(int size, {bool includeUnit = false}) {
+ final sizeInKB = size / 1024.0;
+ if (sizeInKB < 1024.0) {
+ return '${printKB(size, includeUnit: includeUnit)}';
+ } else {
+ return '${printMB(size, fractionDigits: 2, includeUnit: includeUnit)}';
+ }
+}
+
final NumberFormat _kbPattern = NumberFormat.decimalPattern()
..maximumFractionDigits = 0;
-String printKb(num bytes) {
+String printKB(num bytes, {bool includeUnit = false}) {
// We add ((1024/2)-1) to the value before formatting so that a non-zero byte
// value doesn't round down to 0.
- return _kbPattern.format((bytes + 511) / 1024);
+ var output = _kbPattern.format((bytes + 511) / 1024);
+ if (includeUnit) {
+ output += ' KB';
+ }
+ return output;
}
-String printMb(num bytes, [int fractionDigits = 1]) {
- return (bytes / (1024 * 1024.0)).toStringAsFixed(fractionDigits);
+String printMB(num bytes, {int fractionDigits = 1, bool includeUnit = false}) {
+ var output = (bytes / (1024 * 1024.0)).toStringAsFixed(fractionDigits);
+ if (includeUnit) {
+ output += ' MB';
+ }
+ return output;
}
String msText(
diff --git a/packages/devtools_app/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/devtools_app/macos/Flutter/GeneratedPluginRegistrant.swift
index 8236f57..4618f38 100644
--- a/packages/devtools_app/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/packages/devtools_app/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,8 +5,10 @@
import FlutterMacOS
import Foundation
+import path_provider_macos
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
diff --git a/packages/devtools_app/test/timeline_flame_chart_test.dart b/packages/devtools_app/test/timeline_flame_chart_test.dart
index 4052631..5a43356 100644
--- a/packages/devtools_app/test/timeline_flame_chart_test.dart
+++ b/packages/devtools_app/test/timeline_flame_chart_test.dart
@@ -69,10 +69,7 @@
await pumpTimelineBody(tester, TimelineController());
await tester.pumpAndSettle();
expect(find.byType(TimelineFlameChart), findsNothing);
- expect(
- find.byKey(TimelineScreen.emptyTimelineKey),
- findsOneWidget,
- );
+ expect(find.byKey(TimelineScreen.emptyTimelineKey), findsOneWidget);
});
testWidgetsWithWindowSize(
@@ -85,6 +82,7 @@
await tester.pumpAndSettle();
expect(find.byType(TimelineFlameChart), findsOneWidget);
+<<<<<<< HEAD
await expectLater(
find.byType(TimelineFlameChart),
matchesGoldenFile(
@@ -93,5 +91,12 @@
// Await delay for golden comparison.
await tester.pumpAndSettle(const Duration(seconds: 2));
}, skip: kIsWeb || !Platform.isMacOS);
+=======
+ expect(
+ find.byType(TimelineFlameChart),
+ matchesGoldenFile(
+ 'goldens/timeline_flame_chart_with_selected_frame.png'));
+ });
+>>>>>>> 4191e172... Fix merge conflict
});
}
diff --git a/packages/devtools_app/test/utils_test.dart b/packages/devtools_app/test/utils_test.dart
index daaf198..91dd59a 100644
--- a/packages/devtools_app/test/utils_test.dart
+++ b/packages/devtools_app/test/utils_test.dart
@@ -11,27 +11,40 @@
void main() {
group('utils', () {
+ test('prettyPrintBytes', () {
+ const int kb = 1024;
+ const int mb = 1024 * kb;
+
+ expect(prettyPrintBytes(kb), '1');
+ expect(prettyPrintBytes(kb, includeUnit: true), '1 KB');
+ expect(prettyPrintBytes(kb * 1000, includeUnit: true), '1,000 KB');
+
+ expect(prettyPrintBytes(mb), '1.00');
+ expect(prettyPrintBytes(mb, includeUnit: true), '1.00 MB');
+ expect(prettyPrintBytes(mb - kb, includeUnit: true), '1,023 KB');
+ });
+
test('printKb', () {
const int kb = 1024;
- expect(printKb(0), '0');
- expect(printKb(1), '1');
- expect(printKb(kb - 1), '1');
- expect(printKb(kb), '1');
- expect(printKb(kb + 1), '2');
- expect(printKb(2000), '2');
+ expect(printKB(0), '0');
+ expect(printKB(1), '1');
+ expect(printKB(kb - 1), '1');
+ expect(printKB(kb), '1');
+ expect(printKB(kb + 1), '2');
+ expect(printKB(2000), '2');
});
test('printMb', () {
- const int MB = 1024 * 1024;
+ const int mb = 1024 * 1024;
- expect(printMb(10 * MB, 0), '10');
- expect(printMb(10 * MB), '10.0');
- expect(printMb(10 * MB, 2), '10.00');
+ expect(printMB(10 * mb, fractionDigits: 0), '10');
+ expect(printMB(10 * mb), '10.0');
+ expect(printMB(10 * mb, fractionDigits: 2), '10.00');
- expect(printMb(1000 * MB, 0), '1000');
- expect(printMb(1000 * MB), '1000.0');
- expect(printMb(1000 * MB, 2), '1000.00');
+ expect(printMB(1000 * mb, fractionDigits: 0), '1000');
+ expect(printMB(1000 * mb), '1000.0');
+ expect(printMB(1000 * mb, fractionDigits: 2), '1000.00');
});
test('msAsText', () {