Skip to content

Commit

Permalink
Add support for viewing inlining data for code objects in the object …
Browse files Browse the repository at this point in the history
…inspector (#6031)
  • Loading branch information
bkonyi authored Jul 17, 2023
1 parent 88e2e6d commit f4e717c
Show file tree
Hide file tree
Showing 3 changed files with 385 additions and 61 deletions.
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

0 comments on commit f4e717c

Please sign in to comment.