blob: fe44e1cdc827b131a5bdb66634257c9537193e48 [file] [log] [blame]
// Copyright 2018 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:async';
import 'dart:math' as math;
import 'package:vm_service_lib/vm_service_lib.dart';
import '../charts/charts.dart';
import '../framework/framework.dart';
import '../globals.dart';
import '../tables.dart';
import '../ui/elements.dart';
import '../ui/primer.dart';
import '../utils.dart';
import '../vm_service_wrapper.dart';
class PerformanceScreen extends Screen {
PerformanceScreen()
: super(
name: 'Performance',
id: 'performance',
iconClass: 'octicon-dashboard') {
sampleCountStatus = StatusItem();
addStatusItem(sampleCountStatus);
sampleFreqStatus = StatusItem();
addStatusItem(sampleFreqStatus);
}
StatusItem sampleCountStatus;
StatusItem sampleFreqStatus;
PButton loadSnapshotButton;
PButton resetButton;
CoreElement progressElement;
Table<PerfData> perfTable;
CpuChart cpuChart;
SetStateMixin cpuChartStateMixin = SetStateMixin();
CpuTracker cpuTracker;
@override
CoreElement createContent(Framework framework) {
final CoreElement screenDiv = div(c: 'custom-scrollbar')..layoutVertical();
screenDiv.add(<CoreElement>[
createLiveChartArea(),
div(c: 'section'),
div(c: 'section')
..add(<CoreElement>[
form()
..layoutHorizontal()
..clazz('align-items-center')
..add(<CoreElement>[
loadSnapshotButton = PButton('Load snapshot')
..small()
..primary()
..click(_loadSnapshot),
progressElement = span(c: 'margin-left text-gray')..flex(),
resetButton = PButton('Reset VM counters')
..small()
..click(_reset),
])
]),
_createTableView()..clazz('section'),
]);
_updateStatus(null);
serviceManager.isolateManager.onSelectedIsolateChanged.listen((_) {
_handleIsolateChanged();
});
serviceManager.onConnectionAvailable.listen(_handleConnectionStart);
if (serviceManager.hasConnection) {
_handleConnectionStart(serviceManager.service);
}
serviceManager.onConnectionClosed.listen(_handleConnectionStop);
return screenDiv;
}
void _handleIsolateChanged() {
// TODO(devoncarew): Reset UI
}
String get _isolateId => serviceManager.isolateManager.selectedIsolate.id;
void _loadSnapshot() {
loadSnapshotButton.disabled = true;
progressElement.text = 'Loading snapshot…';
serviceManager.service
.getCpuProfile(_isolateId, 'UserVM')
.then((CpuProfile profile) async {
final _CalcProfile calc = _CalcProfile(profile);
await calc.calc();
_updateStatus(profile);
}).catchError((dynamic e) {
framework.showError('Error loading snapshot', e);
}).whenComplete(() {
loadSnapshotButton.disabled = false;
progressElement.text = '';
});
}
CoreElement createLiveChartArea() {
final CoreElement container = div(c: 'section perf-chart table-border')
..layoutVertical();
cpuChart = CpuChart(container);
cpuChart.disabled = true;
return container;
}
void _reset() {
resetButton.disabled = true;
serviceManager.service.clearCpuProfile(_isolateId).then((_) {
framework.toast('VM counters reset.');
}).catchError((dynamic e) {
framework.showError('Error resetting counters', e);
}).whenComplete(() {
resetButton.disabled = false;
});
}
CoreElement _createTableView() {
perfTable = Table<PerfData>.virtual();
perfTable.addColumn(PerfColumnInclusive());
perfTable.addColumn(PerfColumnSelf());
perfTable.addColumn(PerfColumnMethodName());
perfTable.setSortColumn(perfTable.columns.first);
perfTable.setRows(<PerfData>[]);
perfTable.onSelect.listen((PerfData data) {
// TODO(devoncarew): Handle item selection
print(data);
});
return perfTable.element;
}
void _updateStatus(CpuProfile profile) {
if (profile == null) {
sampleCountStatus.element.text = '';
sampleFreqStatus.element.text = '';
} else {
final Duration timeSpan = Duration(seconds: profile.timeSpan.round());
String s = timeSpan.toString();
s = s.substring(0, s.length - 7);
sampleCountStatus.element.text =
'${nf.format(profile.sampleCount)} samples over $s';
sampleFreqStatus.element.text =
'${profile.stackDepth} frames per sample @ ${profile.samplePeriod}Hz';
_process(profile);
}
}
void _process(CpuProfile profile) {
perfTable.setRows(
List<PerfData>.from(profile.functions.where((ProfileFunction f) {
return f.inclusiveTicks > 0 || f.exclusiveTicks > 0;
}).map<PerfData>((ProfileFunction f) {
final int count = math.max(1, profile.sampleCount);
return PerfData(
f.kind,
escape(funcRefName(f.function)),
f.exclusiveTicks / count,
f.inclusiveTicks / count,
);
})));
}
void _handleConnectionStart(VmServiceWrapper service) {
cpuChart.disabled = false;
cpuTracker = CpuTracker(service);
cpuTracker.start();
cpuTracker.onChange.listen((Null _) {
cpuChartStateMixin.setState(() {
cpuChart.updateFrom(cpuTracker);
});
});
}
void _handleConnectionStop(dynamic event) {
cpuChart.disabled = true;
cpuTracker?.stop();
}
}
class CpuChart extends LineChart<CpuTracker> {
CpuChart(CoreElement parent) : super(parent, classes: 'perf-chart') {
usageLabel = parent.add(div(c: 'perf-label'));
usageLabel.element.style.right = '0';
}
CoreElement usageLabel;
@override
void update(CpuTracker data) {
if (data.samples.isEmpty || dim == null) {
return;
}
// display the cpu usage
usageLabel.text = '${data._lastValue}%';
// re-render the svg
final int hRange = CpuTracker.kMaxGraphTime.inSeconds;
const int vRange = 100;
chartElement.setInnerHtml('''
<svg viewBox="0 0 0 0 ${dim.x} ${LineChart.fixedHeight}">
<polyline
fill="none"
stroke="#0074d9"
stroke-width="3"
points="${createPoints(data.samples, hRange, vRange)}"/>
</svg>
''');
}
String createPoints(List<int> samples, int hRange, int vRange) {
// 0,120 20,60 40,80 60,20
final List<String> coords = <String>[];
int pos = 0;
for (int i = samples.length - 1; i >= 0; i--) {
final int x = dim.x - (pos * dim.x ~/ hRange);
final int y = dim.y - (samples[i] * dim.y ~/ vRange);
coords.add('$x,$y');
pos++;
}
return coords.join(' ');
}
}
class CpuTracker {
CpuTracker(this.service);
static const Duration kMaxGraphTime = Duration(minutes: 1);
static const Duration kUpdateDelay = Duration(seconds: 1);
VmServiceWrapper service;
Timer _pollingTimer;
final StreamController<Null> _changeController =
StreamController<Null>.broadcast();
List<int> samples = <int>[];
bool get hasConnection => service != null;
Stream<Null> get onChange => _changeController.stream;
void start() {
_pollingTimer = Timer(const Duration(milliseconds: 100), _pollCpu);
}
void _pollCpu() {
if (!hasConnection) {
return;
}
// TODO(devoncarew): Poll the VM for the CPU load.
_pollingTimer = Timer(kUpdateDelay, _pollCpu);
}
void stop() {
_pollingTimer?.cancel();
service = null;
}
int get _lastValue => samples.isEmpty ? null : samples.last;
// ignore: unused_element
void _addSample(int sample) {
samples.add(sample);
while (samples.length > (kMaxGraphTime.inSeconds + 2)) {
samples.removeAt(0);
}
_changeController.add(null);
}
}
class PerfData {
PerfData(this.kind, this.name, this.self, this.inclusive);
final String kind;
final String name;
final double self;
final double inclusive;
@override
String toString() => '[$kind] $name';
}
class PerfColumnInclusive extends Column<PerfData> {
PerfColumnInclusive() : super('Total');
@override
bool get numeric => true;
@override
dynamic getValue(PerfData item) => item.inclusive;
@override
String render(dynamic value) => percent2(value);
}
class PerfColumnSelf extends Column<PerfData> {
PerfColumnSelf() : super('Self');
@override
bool get numeric => true;
@override
dynamic getValue(PerfData item) => item.self;
@override
String render(dynamic value) => percent2(value);
}
class PerfColumnMethodName extends Column<PerfData> {
PerfColumnMethodName() : super('Method', wide: true);
@override
bool get usesHtml => true;
@override
dynamic getValue(PerfData item) {
if (item.kind == 'Dart') {
return item.name;
}
return '${item.name} <span class="function-kind ${item.kind}">${item.kind}</span>';
}
}
class _CalcProfile {
_CalcProfile(this.profile);
final CpuProfile profile;
Future<void> calc() async {
// TODO(devoncarew): Implement code to parse trie graph
//profile.exclusiveCodeTrie;
// tries['exclusiveCodeTrie'] =
// new Uint32List.fromList(profile['exclusiveCodeTrie']);
// tries['inclusiveCodeTrie'] =
// new Uint32List.fromList(profile['inclusiveCodeTrie']);
// tries['exclusiveFunctionTrie'] =
// new Uint32List.fromList(profile['exclusiveFunctionTrie']);
// tries['inclusiveFunctionTrie'] =
// new Uint32List.fromList(profile['inclusiveFunctionTrie']);
}
}
/*
// Process code table.
for (var codeRegion in profile['codes']) {
if (needToUpdate()) {
await signal(count * 100.0 / length);
}
Code code = codeRegion['code'];
assert(code != null);
codes.add(new ProfileCode.fromMap(this, code, codeRegion));
}
// Process function table.
for (var profileFunction in profile['functions']) {
if (needToUpdate()) {
await signal(count * 100 / length);
}
ServiceFunction function = profileFunction['function'];
assert(function != null);
functions.add(
new ProfileFunction.fromMap(this, function, profileFunction));
}
tries['exclusiveCodeTrie'] =
new Uint32List.fromList(profile['exclusiveCodeTrie']);
tries['inclusiveCodeTrie'] =
new Uint32List.fromList(profile['inclusiveCodeTrie']);
tries['exclusiveFunctionTrie'] =
new Uint32List.fromList(profile['exclusiveFunctionTrie']);
tries['inclusiveFunctionTrie'] =
new Uint32List.fromList(profile['inclusiveFunctionTrie']);
*/