Skip to content

Add support for displaying profiler hits for a script in CodeView #4831

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

Merged
merged 6 commits into from
Dec 7, 2022
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
380 changes: 326 additions & 54 deletions packages/devtools_app/lib/src/screens/debugger/codeview.dart

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../../primitives/history_manager.dart';
import '../../shared/globals.dart';
import '../../shared/routing.dart';
import '../../ui/search.dart';
import '../vm_developer/vm_service_private_extensions.dart';
import 'debugger_model.dart';
import 'program_explorer_controller.dart';
import 'syntax_highlighter.dart';
Expand Down Expand Up @@ -81,6 +82,9 @@ class CodeViewController extends DisposableController
ValueListenable<bool> get showCodeCoverage => _showCodeCoverage;
final _showCodeCoverage = ValueNotifier<bool>(false);

ValueListenable<bool> get showProfileInformation => _showProfileInformation;
final _showProfileInformation = ValueNotifier<bool>(false);

/// Specifies which line should have focus applied in [CodeView].
///
/// A line can be focused by invoking `showScriptLocation` with `focusLine`
Expand All @@ -92,6 +96,10 @@ class CodeViewController extends DisposableController
_showCodeCoverage.value = !_showCodeCoverage.value;
}

void toggleShowProfileInformation() {
_showProfileInformation.value = !_showProfileInformation.value;
}

void clearState() {
// It would be nice to not clear the script history but it is currently
// coupled to ScriptRef objects so that is unsafe.
Expand Down Expand Up @@ -157,27 +165,22 @@ class CodeViewController extends DisposableController
scriptsHistory.current.addListener(_scriptHistoryListener);
}

Future<void> refreshCodeCoverage() async {
final hitLines = <int>{};
final missedLines = <int>{};
Future<void> refreshCodeStatistics() async {
final current = parsedScript.value;
if (current == null) {
return;
}
final isolateRef = serviceManager.isolateManager.selectedIsolate.value!;
await _getCoverageReport(
final processedReport = await _getSourceReport(
isolateRef,
current.script,
hitLines,
missedLines,
);

parsedScript.value = ParsedScript(
script: current.script,
highlighter: current.highlighter,
executableLines: current.executableLines,
coverageHitLines: hitLines,
coverageMissedLines: missedLines,
sourceReport: processedReport,
);
}

Expand Down Expand Up @@ -211,28 +214,46 @@ class CodeViewController extends DisposableController
_scriptLocation.value = scriptLocation;
}

Future<void> _getCoverageReport(
Future<ProcessedSourceReport> _getSourceReport(
IsolateRef isolateRef,
ScriptRef script,
Set<int> hitLines,
Set<int> missedLines,
Script script,
) async {
final hitLines = <int>{};
final missedLines = <int>{};
try {
final report = await serviceManager.service!.getSourceReport(
isolateRef.id!,
const [SourceReportKind.kCoverage],
// TODO(bkonyi): make _Profile a public report type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a issue filed for this in the SDK?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// See https://github.com/dart-lang/sdk/issues/50641
const [
SourceReportKind.kCoverage,
'_Profile',
],
scriptId: script.id!,
reportLines: true,
);

for (final range in report.ranges!) {
final coverage = range.coverage!;
hitLines.addAll(coverage.hits!);
missedLines.addAll(coverage.misses!);
}

final profileReport = report.asProfileReport(script);
return ProcessedSourceReport(
coverageHitLines: hitLines,
coverageMissedLines: missedLines,
profilerEntries:
profileReport.profileRanges.fold<Map<int, ProfileReportEntry>>(
{},
(last, e) => last..addAll(e.entries),
),
);
} catch (e) {
// Ignore - not supported for all vm service implementations.
log('$e');
}
return const ProcessedSourceReport.empty();
}

/// Parses the current script into executable lines and prepares the script
Expand All @@ -252,9 +273,6 @@ class CodeViewController extends DisposableController
// Gather the data to display breakable lines.
var executableLines = <int>{};

final hitLines = <int>{};
final missedLines = <int>{};

if (script != null) {
final isolateRef = serviceManager.isolateManager.selectedIsolate.value!;
try {
Expand All @@ -270,19 +288,16 @@ class CodeViewController extends DisposableController
log('$e');
}

await _getCoverageReport(
final processedReport = await _getSourceReport(
isolateRef,
script,
hitLines,
missedLines,
);

parsedScript.value = ParsedScript(
script: script,
highlighter: highlighter,
executableLines: executableLines,
coverageHitLines: hitLines,
coverageMissedLines: missedLines,
sourceReport: processedReport,
);
}
}
Expand Down Expand Up @@ -336,6 +351,23 @@ class CodeViewController extends DisposableController
}
}

class ProcessedSourceReport {
ProcessedSourceReport({
required this.coverageHitLines,
required this.coverageMissedLines,
required this.profilerEntries,
});

const ProcessedSourceReport.empty()
: coverageHitLines = const <int>{},
coverageMissedLines = const <int>{},
profilerEntries = const <int, ProfileReportEntry>{};

final Set<int> coverageHitLines;
final Set<int> coverageMissedLines;
final Map<int, ProfileReportEntry> profilerEntries;
}

/// Maintains the navigation history of the debugger's code area - which files
/// were opened, whether it's possible to navigate forwards and backwards in the
/// history, ...
Expand Down Expand Up @@ -368,8 +400,7 @@ class ParsedScript {
required this.script,
required this.highlighter,
required this.executableLines,
required this.coverageHitLines,
required this.coverageMissedLines,
required this.sourceReport,
}) : lines = (script.source?.split('\n') ?? const []).toList();

final Script script;
Expand All @@ -378,8 +409,7 @@ class ParsedScript {

final Set<int> executableLines;

final Set<int> coverageHitLines;
final Set<int> coverageMissedLines;
final ProcessedSourceReport? sourceReport;

final List<String> lines;

Expand Down
37 changes: 25 additions & 12 deletions packages/devtools_app/lib/src/screens/debugger/controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import 'debugger_controller.dart';
class DebuggingControls extends StatefulWidget {
const DebuggingControls({Key? key}) : super(key: key);

static const minWidthBeforeScaling = 1600.0;
static const minWidthBeforeScaling = 1750.0;

@override
_DebuggingControlsState createState() => _DebuggingControlsState();
Expand Down Expand Up @@ -57,7 +57,7 @@ class _DebuggingControlsState extends State<DebuggingControls>
BreakOnExceptionsControl(controller: controller),
if (isVmApp) ...[
const SizedBox(width: denseSpacing),
CodeCoverageToggle(controller: controller),
CodeStatisticsControls(controller: controller),
],
const Expanded(child: SizedBox(width: denseSpacing)),
_librariesButton(),
Expand Down Expand Up @@ -144,8 +144,8 @@ class _DebuggingControlsState extends State<DebuggingControls>
}
}

class CodeCoverageToggle extends StatelessWidget {
const CodeCoverageToggle({
class CodeStatisticsControls extends StatelessWidget {
const CodeStatisticsControls({
super.key,
required this.controller,
});
Expand All @@ -155,18 +155,19 @@ class CodeCoverageToggle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RoundedOutlinedBorder(
child: ValueListenableBuilder<bool>(
valueListenable: controller.codeViewController.showCodeCoverage,
builder: (context, selected, _) {
final isInSmallMode = MediaQuery.of(context).size.width <
child: DualValueListenableBuilder<bool, bool>(
firstListenable: controller.codeViewController.showCodeCoverage,
secondListenable: controller.codeViewController.showProfileInformation,
builder: (context, showCodeCoverage, showProfileInformation, _) {
final isInSmallMode = MediaQuery.of(context).size.width <=
DebuggingControls.minWidthBeforeScaling;
return Row(
children: [
ToggleButton(
label: isInSmallMode ? null : 'Show Coverage',
message: 'Show code coverage',
icon: Codicons.checklist,
isSelected: selected,
isSelected: showCodeCoverage,
outlined: false,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
Expand All @@ -176,13 +177,25 @@ class CodeCoverageToggle extends StatelessWidget {
),
onPressed: controller.codeViewController.toggleShowCodeCoverage,
),
LeftBorder(
child: ToggleButton(
label: isInSmallMode ? null : 'Show Profile',
message: 'Show profiler hits',
icon: Codicons.flame,
isSelected: showProfileInformation,
outlined: false,
shape: const ContinuousRectangleBorder(),
onPressed: controller
.codeViewController.toggleShowProfileInformation,
),
),
LeftBorder(
child: IconLabelButton(
label: '',
tooltip: 'Refresh code coverage statistics',
tooltip: 'Refresh statistics',
outlined: false,
onPressed: selected
? controller.codeViewController.refreshCodeCoverage
onPressed: showCodeCoverage || showProfileInformation
? controller.codeViewController.refreshCodeStatistics
: null,
minScreenWidthForTextBeforeScaling: 20000,
icon: Icons.refresh,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,10 @@ class _CpuProfilerState extends State<CpuProfiler>
isFilterActive: widget.controller.isToggleFilterActive,
),
const SizedBox(width: denseSpacing),
if (currentTab.key != CpuProfiler.flameChartTab)
if (currentTab.key != CpuProfiler.flameChartTab) ...[
const DisplayTreeGuidelinesToggle(),
const SizedBox(width: denseSpacing),
const SizedBox(width: denseSpacing),
],
UserTagDropdown(widget.controller),
const SizedBox(width: denseSpacing),
ValueListenableBuilder<bool>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,92 @@ extension CpuSamplesPrivateView on CpuSamples {
return _codes!;
}
}

extension ProfileDataRanges on SourceReport {
ProfileReport asProfileReport(Script script) =>
ProfileReport._fromJson(script, json!);
}

class ProfileReportMetaData {
const ProfileReportMetaData._({required this.sampleCount});
final int sampleCount;
}

/// Profiling information for a given line in a [Script].
class ProfileReportEntry {
const ProfileReportEntry({
required this.sampleCount,
required this.line,
required this.inclusive,
required this.exclusive,
});

final int sampleCount;
final int line;
final int inclusive;
final int exclusive;

double get inclusivePercentage => inclusive * 100 / sampleCount;
double get exclusivePercentage => exclusive * 100 / sampleCount;
}

/// Profiling information for a range of token positions in a [Script].
class ProfileReportRange {
ProfileReportRange._fromJson(Script script, Map<String, dynamic> json)
: metadata = ProfileReportMetaData._(
sampleCount: json[_kProfileKey][_kMetadataKey][_kSampleCountKey],
),
inclusiveTicks = json[_kProfileKey][_kInclusiveTicksKey].cast<int>(),
exclusiveTicks = json[_kProfileKey][_kExclusiveTicksKey].cast<int>(),
lines = json[_kProfileKey][_kPositionsKey]
.map<int>(
// It's possible to get a synthetic token position which will
// either be a negative value or a String (e.g., 'ParallelMove'
// or 'NoSource'). We'll just use -1 as a placeholder since we
// won't display anything for these tokens anyway.
(e) => e is int
? script.getLineNumberFromTokenPos(e) ?? _kNoSourcePosition
: _kNoSourcePosition,
)
.toList() {
for (int i = 0; i < lines.length; ++i) {
final line = lines[i];
entries[line] = ProfileReportEntry(
sampleCount: metadata.sampleCount,
line: line,
inclusive: inclusiveTicks[i],
exclusive: exclusiveTicks[i],
);
}
}

static const _kProfileKey = 'profile';
static const _kMetadataKey = 'metadata';
static const _kSampleCountKey = 'sampleCount';
static const _kInclusiveTicksKey = 'inclusiveTicks';
static const _kExclusiveTicksKey = 'exclusiveTicks';
static const _kPositionsKey = 'positions';
static const _kNoSourcePosition = -1;

final ProfileReportMetaData metadata;
final entries = <int, ProfileReportEntry>{};
List<int> inclusiveTicks;
List<int> exclusiveTicks;
List<int> lines;
}

/// A representation of the `_Profile` [SourceReport], which contains profiling
/// information for a given [Script].
class ProfileReport {
ProfileReport._fromJson(Script script, Map<String, dynamic> json)
: _profileRanges = (json['ranges'] as List)
.cast<Map<String, dynamic>>()
.where((e) => e.containsKey('profile'))
.map<ProfileReportRange>(
(e) => ProfileReportRange._fromJson(script, e),
)
.toList();

List<ProfileReportRange> get profileRanges => _profileRanges;
final List<ProfileReportRange> _profileRanges;
}
1 change: 1 addition & 0 deletions packages/devtools_app/lib/src/shared/common_widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const debuggerDeviceWidth = 800.0;

const defaultDialogRadius = 20.0;
double get areaPaneHeaderHeight => scaleByFontFactor(36.0);
double get assumedMonospaceCharacterWidth => scaleByFontFactor(9.0);

/// Convenience [Divider] with [Padding] that provides a good divider in forms.
class PaddedDivider extends StatelessWidget {
Expand Down
Loading