blob: b478e982a039cc2c9a807923f5194752b09d8ab5 [file] [log] [blame]
// Copyright 2019 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 'package:meta/meta.dart';
import '../charts/flame_chart_canvas.dart';
import '../ui/colors.dart';
import '../ui/elements.dart';
import '../ui/fake_flutter/dart_ui/dart_ui.dart';
import '../ui/fake_flutter/fake_flutter.dart';
import '../ui/flutter_html_shim.dart';
import '../ui/theme.dart';
import 'cpu_profile_model.dart';
import 'cpu_profiler.dart';
class CpuFlameChart extends CpuProfilerView {
CpuFlameChart(CpuProfileDataProvider profileDataProvider)
: super(CpuProfilerViewType.flameChart, profileDataProvider) {
stackFrameDetails = div(c: 'event-details-heading stack-frame-details')
..element.style.backgroundColor = colorToCss(stackFrameDetailsBackground)
..hidden(true);
add(stackFrameDetails);
}
static const String stackFrameDetailsDefaultText =
'[No stack frame selected]';
static const stackFrameDetailsBackground = ThemedColor(
Color(0xFFF6F6F6),
Color(0xFF202124),
);
CpuFlameChartCanvas canvas;
CoreElement stackFrameDetails;
@override
void rebuildView() {
final CpuProfileData data = profileDataProvider();
canvas = CpuFlameChartCanvas(
data: data,
width: element.clientWidth.toDouble(),
height: math.max(
// Subtract [rowHeightWithPadding] to account for timeline at the top of
// the flame chart.
element.clientHeight - rowHeightWithPadding,
// Add 1 to account for a row of padding at the bottom of the chart.
(data.cpuProfileRoot.depth + 1) * rowHeightWithPadding,
),
);
canvas.onNodeSelected.listen((node) {
assert(node.data is CpuStackFrame);
stackFrameDetails.text = node.data.toString();
});
add(canvas.element);
stackFrameDetails
..text = stackFrameDetailsDefaultText
..hidden(false);
}
@override
void update({bool showLoadingSpinner = false}) {
reset();
super.update(showLoadingSpinner: showLoadingSpinner);
}
void updateForContainerResize() {
if (canvas == null) {
return;
}
final data = profileDataProvider();
// Only update the canvas if the flame chart is visible and has data.
// Otherwise, mark the canvas as needing a rebuild.
if (!isHidden && data != null) {
// We need to rebuild the canvas with a new content size so that the
// canvas is always at least as tall as the container it is in. This
// ensures that the grid lines in the chart will extend all the way to the
// bottom of the container.
canvas.forceRebuildForSize(
canvas.widthWithInsets,
math.max(
// Subtract [rowHeightWithPadding] to account for the size of
// [stackFrameDetails] section at the bottom of the chart.
element.scrollHeight.toDouble() - rowHeightWithPadding,
// Add 1 to account for a row of padding at the bottom of the chart.
(data.cpuProfileRoot.depth + 1) * rowHeightWithPadding,
),
);
} else {
viewNeedsRebuild = true;
}
}
@override
void reset() {
if (canvas?.element?.element != null) {
canvas.element.element.remove();
}
canvas = null;
stackFrameDetails.text = stackFrameDetailsDefaultText;
stackFrameDetails.hidden(true);
}
}
class CpuFlameChartCanvas extends FlameChartCanvas<CpuProfileData> {
CpuFlameChartCanvas({
@required CpuProfileData data,
@required double width,
@required double height,
}) : super(
data: data,
duration: data.profileMetaData.time.duration,
width: width,
height: height,
classes: 'cpu-flame-chart',
);
static const stackFramePadding = 1;
int _colorOffset = 0;
@override
double get calculatedWidth => rows[0].nodes[0].rect.right - sideInset;
@override
void initRows() {
for (int i = 0; i < data.cpuProfileRoot.depth; i++) {
rows.add(FlameChartRow(nodes: [], index: i));
}
final totalWidth = width - 2 * sideInset;
final Map<String, double> stackFrameLefts = {};
double leftForStackFrame(CpuStackFrame stackFrame) {
final CpuStackFrame parent = stackFrame.parent;
double left;
if (parent == null) {
left = sideInset;
} else {
final stackFrameIndex = stackFrame.index;
if (stackFrameIndex == 0) {
// This is the first child of parent. [left] should equal the left
// value of [stackFrame]'s parent.
left = stackFrameLefts[parent.id];
} else {
assert(stackFrameIndex != -1);
// [stackFrame] is not the first child of its parent. [left] should
// equal the right value of its previous sibling.
final CpuStackFrame previous = parent.children[stackFrameIndex - 1];
left = stackFrameLefts[previous.id] +
(totalWidth * previous.totalTimeRatio);
}
}
stackFrameLefts[stackFrame.id] = left;
return left;
}
void createChartNodes(CpuStackFrame stackFrame, int row) {
final double width =
totalWidth * stackFrame.totalTimeRatio - stackFramePadding;
final left = leftForStackFrame(stackFrame);
final top = row * rowHeightWithPadding + topOffset;
final backgroundColor = _colorForStackFrame(stackFrame);
final node = FlameChartNode<CpuStackFrame>(
Rect.fromLTRB(left, top, left + width, top + rowHeight),
backgroundColor,
Colors.black,
Colors.black,
stackFrame,
(_) => stackFrame.name,
);
rows[row].nodes.add(node);
for (CpuStackFrame child in stackFrame.children) {
createChartNodes(
child,
row + 1,
);
}
}
createChartNodes(data.cpuProfileRoot, 0);
}
// TODO(kenzie): base colors on categories (Widget, Render, Layer, User code,
// etc.)
Color _colorForStackFrame(CpuStackFrame stackFrame) {
final color = uiColorPalette[_colorOffset % uiColorPalette.length];
_colorOffset++;
return color;
}
}