Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for viewing inlining data for code objects in the object inspector #6031

Merged
merged 5 commits into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ import '../vm_service_private_extensions.dart';
import 'object_inspector_view_controller.dart';
import 'vm_object_model.dart';

abstract class _CodeColumnData extends ColumnData<Instruction> {
abstract class _CodeColumnData<T> extends ColumnData<T> {
_CodeColumnData(super.title, {required super.fixedWidthPx});
_CodeColumnData.wide(super.title) : super.wide();

@override
bool get supportsSorting => false;
}

class _AddressColumn extends _CodeColumnData {
class _AddressColumn extends _CodeColumnData<Instruction> {
_AddressColumn()
: super(
'Address',
Expand All @@ -44,13 +44,61 @@ class _AddressColumn extends _CodeColumnData {
}
}

class _AddressRangeColumn extends _CodeColumnData<InliningEntry> {
_AddressRangeColumn()
: super(
'Address Range',
fixedWidthPx: 300,
);

@override
String getValue(InliningEntry dataObject) {
return '[${dataObject.addressRange.begin.asAddress}, '
'${dataObject.addressRange.end.asAddress})';
}
}

class _FunctionsColumn extends _CodeColumnData<InliningEntry>
implements ColumnRenderer<InliningEntry> {
_FunctionsColumn({required this.controller}) : super.wide('Functions');

final ObjectInspectorViewController controller;

@override
Widget? build(
BuildContext context,
InliningEntry data, {
bool isRowSelected = false,
VoidCallback? onPressed,
}) {
return Row(
children: [
for (final function in data.functions) ...[
VmServiceObjectLink(
object: function,
onTap: controller.findAndSelectNodeForObject,
),
const SizedBox(
width: denseSpacing,
),
],
],
);
}

@override
Object? getValue(InliningEntry dataObject) {
return dataObject;
}
}

// TODO(bkonyi): consider coloring the background similarly to how we indicate
// code "hotness" in the debugger tab. To do this properly here, we'd need to
// modify the table column padding logic to allow for custom column rendering
// that can fill the entire column which is a can of worms I'd rather not open
// for some rather niche functionality. We can revisit this once we can use the
// table implementation from the Flutter framework.
class _ProfileTicksColumn extends _CodeColumnData {
class _ProfileTicksColumn extends _CodeColumnData<Instruction> {
_ProfileTicksColumn(
super.title, {
required this.inclusive,
Expand All @@ -77,7 +125,41 @@ class _ProfileTicksColumn extends _CodeColumnData {
}
}

class _InstructionColumn extends _CodeColumnData
// TODO(bkonyi): consider coloring the background similarly to how we indicate
// code "hotness" in the debugger tab. To do this properly here, we'd need to
// modify the table column padding logic to allow for custom column rendering
// that can fill the entire column which is a can of worms I'd rather not open
// for some rather niche functionality. We can revisit this once we can use the
// table implementation from the Flutter framework.
class _ProfileRangeTicksColumn extends _CodeColumnData<InliningEntry> {
_ProfileRangeTicksColumn(
super.title, {
required this.inclusive,
required this.ticks,
}) : super(fixedWidthPx: 140);

final bool inclusive;
final CpuProfilerTicksTable? ticks;

@override
int? getValue(InliningEntry dataObject) {
if (ticks == null) return null;
final range = dataObject.addressRange;
final tick = ticks!.forRange(range.begin.toInt(), range.end.toInt());
return inclusive ? tick?.inclusiveTicks : tick?.exclusiveTicks;
}

@override
String getDisplayValue(InliningEntry dataObject) {
final value = getValue(dataObject);
if (value == null) return '';

final percentage = percent(value / ticks!.sampleCount);
return '$percentage ($value)';
}
}

class _InstructionColumn extends _CodeColumnData<Instruction>
implements ColumnRenderer<Instruction> {
_InstructionColumn()
: super(
Expand Down Expand Up @@ -181,7 +263,7 @@ class _InstructionColumn extends _CodeColumnData
}
}

class _DartObjectColumn extends _CodeColumnData
class _DartObjectColumn extends _CodeColumnData<Instruction>
implements ColumnRenderer<Instruction> {
_DartObjectColumn({required this.controller}) : super.wide('Object');

Expand Down Expand Up @@ -233,10 +315,26 @@ class VmCodeDisplay extends StatelessWidget {
),
),
OutlineDecoration.onlyTop(
child: CodeTable(
code: code,
controller: controller,
ticks: code.ticksTable,
child: Column(
children: [
if (code.obj.hasInliningData) ...[
Flexible(
child: InliningTable(
code: code,
controller: controller,
ticks: code.ticksTable,
),
),
const ThickDivider(),
],
Flexible(
child: CodeTable(
code: code,
controller: controller,
ticks: code.ticksTable,
),
),
],
),
),
],
Expand Down Expand Up @@ -267,6 +365,61 @@ class VmCodeDisplay extends StatelessWidget {
}
}

class InliningTable extends StatelessWidget {
InliningTable({
Key? key,
required this.code,
required this.controller,
required this.ticks,
}) : inliningData = code.obj.inliningData,
super(key: key);

final CodeObject code;
final InliningData inliningData;
final ObjectInspectorViewController controller;
final CpuProfilerTicksTable? ticks;

late final columns = <ColumnData<InliningEntry>>[
_AddressRangeColumn(),
_FunctionsColumn(controller: controller),
if (ticks != null) ...[
_ProfileRangeTicksColumn(
'Total %',
ticks: code.ticksTable,
inclusive: true,
),
_ProfileRangeTicksColumn(
'Self %',
ticks: code.ticksTable,
inclusive: false,
),
],
];

@override
Widget build(BuildContext context) {
return FlatTable<InliningEntry>(
data: inliningData.entries,
dataKey: 'vm-code-display',
keyFactory: (entry) => Key(entry.addressRange.toString()),
columnGroups: [
ColumnGroup.fromText(
title: 'Inlined Functions',
range: const Range(0, 2),
),
if (ticks != null)
ColumnGroup.fromText(
title: 'Profiler Ticks',
range: const Range(2, 4),
),
],
columns: columns,
defaultSortColumn: columns[0],
defaultSortDirection: SortDirection.ascending,
);
}
}

class CodeTable extends StatelessWidget {
CodeTable({
Key? key,
Expand Down Expand Up @@ -345,6 +498,19 @@ class CpuProfilerTicksTable {
/// returned.
CodeTicks? operator [](String address) => _table[address];

CodeTicks? forRange(int start, int end) {
CodeTicks? result;
for (int i = start; i < end; ++i) {
final ticks = this[i.toRadixString(16)];
if (result == null) {
result = ticks;
} else if (ticks != null) {
result += ticks;
}
}
return result;
}

final _table = <String, CodeTicks>{};
}

Expand All @@ -356,6 +522,11 @@ class CodeTicks {
required this.exclusiveTicks,
});

CodeTicks operator +(CodeTicks other) => CodeTicks(
inclusiveTicks: inclusiveTicks + other.inclusiveTicks,
exclusiveTicks: exclusiveTicks + other.exclusiveTicks,
);

final int exclusiveTicks;
final int inclusiveTicks;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

// ignore_for_file: constant_identifier_names

import 'package:flutter/widgets.dart';
import 'package:vm_service/vm_service.dart';

import '../../shared/globals.dart';
import '../../shared/primitives/utils.dart';
import '../memory/panes/profile/profile_view.dart';

/// NOTE: this file contains extensions to classes provided by
Expand Down Expand Up @@ -598,6 +600,71 @@ extension CodePrivateViewExtension on Code {
String get kind => json![_kindKey];

ObjectPoolRef get objectPool => ObjectPoolRef.parse(json![_objectPoolKey]);

bool get hasInliningData => json!.containsKey(InliningData.kInlinedFunctions);
InliningData get inliningData => InliningData.parse(json!);
}

extension AddressExtension on num {
String get asAddress =>
'0x${toInt().toRadixString(16).toUpperCase().padLeft(8, '0')}';
}

class InliningData {
const InliningData._({required this.entries});

factory InliningData.parse(Map<String, dynamic> json) {
final startAddress = int.parse(json[kStartAddressKey], radix: 16);
final intervals = json[kInlinedIntervals] as List;
final functions = (json[kInlinedFunctions] as List)
.cast<Map<String, dynamic>>()
.map<FuncRef>((e) => FuncRef.parse(e)!)
.toList();

final entries = <InliningEntry>[];

// Inlining data format: [startAddress, endAddress, 0, inline functions...]
for (final interval in intervals) {
assert(interval.length >= 2);
final range = Range(
startAddress + interval[0],
startAddress + interval[1],
);
// We start at i = 3 as `interval[2]` is always present and set to 0,
// likely serving as a sentinel. `functions[0]` is not inlined for every
// range, so we'll ignore this value.
final inlinedFunctions = <FuncRef>[
for (int i = 3; i < interval.length; ++i) functions[interval[i]],
];
entries.add(
InliningEntry(
addressRange: range,
functions: inlinedFunctions,
),
);
}

return InliningData._(entries: entries);
}

@visibleForTesting
static const kInlinedIntervals = '_inlinedIntervals';
@visibleForTesting
static const kInlinedFunctions = '_inlinedFunctions';
@visibleForTesting
static const kStartAddressKey = '_startAddress';

final List<InliningEntry> entries;
}

class InliningEntry {
const InliningEntry({
required this.addressRange,
required this.functions,
});

final Range addressRange;
final List<FuncRef> functions;
}

class ObjectPoolRef extends ObjRef {
Expand Down
Loading