blob: 34e5b38dc5210b241fa0455df2110ce725b1bff3 [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:collection';
import 'dart:convert';
import 'package:meta/meta.dart';
import '../timeline/timeline_model.dart';
import '../trees.dart';
import '../utils.dart';
/// Data model for DevTools CPU profile.
class CpuProfileData {
CpuProfileData._({
@required this.stackFramesJson,
@required this.stackTraceEvents,
@required this.profileMetaData,
}) {
_cpuProfileRoot = CpuStackFrame(
id: 'cpuProfile',
name: 'all',
category: 'Dart',
url: '',
profileMetaData: profileMetaData,
);
}
static CpuProfileData parse(Map<String, dynamic> json) {
return CpuProfileData._(
stackFramesJson: jsonDecode(jsonEncode(json[stackFramesKey] ?? {})),
stackTraceEvents:
(json[traceEventsKey] ?? []).cast<Map<String, dynamic>>(),
profileMetaData: CpuProfileMetaData(
sampleCount: json[sampleCountKey],
samplePeriod: json[samplePeriodKey],
stackDepth: json[stackDepthKey],
time: (json[timeOriginKey] != null && json[timeExtentKey] != null)
? (TimeRange()
..start = Duration(microseconds: json[timeOriginKey])
..end = Duration(
microseconds: json[timeOriginKey] + json[timeExtentKey]))
: null,
),
);
}
static CpuProfileData subProfile(
CpuProfileData superProfile,
TimeRange subTimeRange,
) {
// Each trace event in [subTraceEvents] will have the leaf stack frame id
// for a cpu sample within [subTimeRange].
final subTraceEvents = superProfile.stackTraceEvents
.where((trace) => subTimeRange
.contains(Duration(microseconds: trace[TraceEvent.timestampKey])))
.toList();
// Use a SplayTreeMap so that map iteration will be in sorted key order.
final SplayTreeMap<String, Map<String, dynamic>> subStackFramesJson =
SplayTreeMap(stackFrameIdCompare);
for (Map<String, dynamic> traceEvent in subTraceEvents) {
// Add leaf frame.
final String leafId = traceEvent[stackFrameIdKey];
final Map<String, dynamic> leafFrameJson =
superProfile.stackFramesJson[leafId];
subStackFramesJson[leafId] = leafFrameJson;
// Add leaf frame's ancestors.
String parentId = leafFrameJson[parentIdKey];
while (parentId != null) {
final parentFrameJson = superProfile.stackFramesJson[parentId];
subStackFramesJson[parentId] = parentFrameJson;
parentId = parentFrameJson[parentIdKey];
}
}
return CpuProfileData._(
stackFramesJson: subStackFramesJson,
stackTraceEvents: subTraceEvents,
profileMetaData: CpuProfileMetaData(
sampleCount: subTraceEvents.length,
samplePeriod: superProfile.profileMetaData.samplePeriod,
stackDepth: superProfile.profileMetaData.stackDepth,
time: subTimeRange,
),
);
}
// Key fields from the VM response JSON.
static const nameKey = 'name';
static const categoryKey = 'category';
static const parentIdKey = 'parent';
static const stackFrameIdKey = 'sf';
static const resolvedUrlKey = 'resolvedUrl';
static const stackFramesKey = 'stackFrames';
static const traceEventsKey = 'traceEvents';
static const sampleCountKey = 'sampleCount';
static const stackDepthKey = 'stackDepth';
static const samplePeriodKey = 'samplePeriod';
static const timeOriginKey = 'timeOriginMicros';
static const timeExtentKey = 'timeExtentMicros';
/// Marks whether this data has already been processed.
bool processed = false;
final Map<String, dynamic> stackFramesJson;
/// Trace events associated with the last stackFrame in each sample (i.e. the
/// leaves of the [CpuStackFrame] objects).
///
/// The trace event will contain a field 'sf' that contains the id of the leaf
/// stack frame.
final List<Map<String, dynamic>> stackTraceEvents;
final CpuProfileMetaData profileMetaData;
CpuStackFrame get cpuProfileRoot => _cpuProfileRoot;
CpuStackFrame _cpuProfileRoot;
Map<String, CpuStackFrame> stackFrames = {};
Map<String, dynamic> get json => {
'type': '_CpuProfileTimeline',
samplePeriodKey: profileMetaData.samplePeriod,
sampleCountKey: profileMetaData.sampleCount,
stackDepthKey: profileMetaData.stackDepth,
timeOriginKey: profileMetaData.time.start.inMicroseconds,
timeExtentKey: profileMetaData.time.duration.inMicroseconds,
stackFramesKey: stackFramesJson,
traceEventsKey: stackTraceEvents,
};
}
class CpuProfileMetaData {
CpuProfileMetaData({
@required this.sampleCount,
@required this.samplePeriod,
@required this.stackDepth,
@required this.time,
});
final int sampleCount;
final int samplePeriod;
final int stackDepth;
final TimeRange time;
}
class CpuStackFrame extends TreeNode<CpuStackFrame> {
CpuStackFrame({
@required this.id,
@required this.name,
@required this.category,
@required this.url,
@required this.profileMetaData,
});
final String id;
final String name;
final String category;
final String url;
final CpuProfileMetaData profileMetaData;
/// How many cpu samples for which this frame is a leaf.
int exclusiveSampleCount = 0;
int get inclusiveSampleCount =>
_inclusiveSampleCount ?? _calculateInclusiveSampleCount();
/// How many cpu samples this frame is included in.
int _inclusiveSampleCount;
set inclusiveSampleCount(int count) => _inclusiveSampleCount = count;
double get totalTimeRatio =>
_totalTimeRatio ??= inclusiveSampleCount / profileMetaData.sampleCount;
double _totalTimeRatio;
Duration get totalTime => _totalTime ??= Duration(
microseconds:
(totalTimeRatio * profileMetaData.time.duration.inMicroseconds)
.round());
Duration _totalTime;
double get selfTimeRatio =>
_selfTimeRatio ??= exclusiveSampleCount / profileMetaData.sampleCount;
double _selfTimeRatio;
Duration get selfTime => _selfTime ??= Duration(
microseconds:
(selfTimeRatio * profileMetaData.time.duration.inMicroseconds)
.round());
Duration _selfTime;
/// Returns the number of cpu samples this stack frame is a part of.
///
/// This will be equal to the number of leaf nodes under this stack frame.
int _calculateInclusiveSampleCount() {
int count = exclusiveSampleCount;
for (CpuStackFrame child in children) {
count += child.inclusiveSampleCount;
}
_inclusiveSampleCount = count;
return _inclusiveSampleCount;
}
CpuStackFrame shallowCopy({bool resetInclusiveSampleCount = false}) {
final copy = CpuStackFrame(
id: id,
name: name,
category: category,
url: url,
profileMetaData: profileMetaData,
)
..exclusiveSampleCount = exclusiveSampleCount
..inclusiveSampleCount =
resetInclusiveSampleCount ? null : inclusiveSampleCount;
return copy;
}
/// Returns a deep copy from this stack frame down to the leaves of the tree.
///
/// The returned copy stack frame will have a null parent.
CpuStackFrame deepCopy() {
final copy = shallowCopy();
for (CpuStackFrame child in children) {
copy.addChild(child.deepCopy());
}
return copy;
}
/// Whether [this] stack frame matches another stack frame [other].
///
/// Two stack frames are said to be matching if they share the following
/// properties.
bool matches(CpuStackFrame other) =>
name == other.name && url == other.url && category == other.category;
void _format(StringBuffer buf, String indent) {
buf.writeln('$indent$name - children: ${children.length} - excl: '
'$exclusiveSampleCount - incl: $inclusiveSampleCount'
.trimRight());
for (CpuStackFrame child in children) {
child._format(buf, ' $indent');
}
}
@visibleForTesting
String toStringDeep() {
final buf = StringBuffer();
_format(buf, ' ');
return buf.toString();
}
@override
String toString() {
final buf = StringBuffer();
buf.write('$name ');
if (totalTime != null) {
// TODO(kenzie): use a number of fractionDigits that better matches the
// resolution of the stack frame.
buf.write('- ${msText(totalTime, fractionDigits: 2)} ');
}
buf.write('($inclusiveSampleCount ');
buf.write(inclusiveSampleCount == 1 ? 'sample' : 'samples');
buf.write(', ${percent2(totalTimeRatio)})');
return buf.toString();
}
}
@visibleForTesting
int stackFrameIdCompare(String a, String b) {
// Stack frame ids are structured as 140225212960768-24 (iOS) or -784070656-24
// (Android). We need to compare the number after the last dash to maintain
// the correct order.
const dash = '-';
final aDashIndex = a.lastIndexOf(dash);
final bDashIndex = b.lastIndexOf(dash);
try {
final int aId = int.parse(a.substring(aDashIndex + 1));
final int bId = int.parse(b.substring(bDashIndex + 1));
return aId.compareTo(bId);
} catch (e) {
String error = 'invalid stack frame ';
if (aDashIndex == -1 && bDashIndex != -1) {
error += 'id [$a]';
} else if (aDashIndex != -1 && bDashIndex == -1) {
error += 'id [$b]';
} else {
error += 'ids [$a, $b]';
}
throw error;
}
}