Skip to content

Commit f9c1eeb

Browse files
authored
Add support for displaying profiler hits for a script in CodeView (#4831)
Fixes #4750
1 parent 0982e23 commit f9c1eeb

File tree

15 files changed

+603
-112
lines changed

15 files changed

+603
-112
lines changed

packages/devtools_app/lib/src/screens/debugger/codeview.dart

Lines changed: 326 additions & 54 deletions
Large diffs are not rendered by default.

packages/devtools_app/lib/src/screens/debugger/codeview_controller.dart

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import '../../primitives/history_manager.dart';
1313
import '../../shared/globals.dart';
1414
import '../../shared/routing.dart';
1515
import '../../ui/search.dart';
16+
import '../vm_developer/vm_service_private_extensions.dart';
1617
import 'debugger_model.dart';
1718
import 'program_explorer_controller.dart';
1819
import 'syntax_highlighter.dart';
@@ -81,6 +82,9 @@ class CodeViewController extends DisposableController
8182
ValueListenable<bool> get showCodeCoverage => _showCodeCoverage;
8283
final _showCodeCoverage = ValueNotifier<bool>(false);
8384

85+
ValueListenable<bool> get showProfileInformation => _showProfileInformation;
86+
final _showProfileInformation = ValueNotifier<bool>(false);
87+
8488
/// Specifies which line should have focus applied in [CodeView].
8589
///
8690
/// A line can be focused by invoking `showScriptLocation` with `focusLine`
@@ -92,6 +96,10 @@ class CodeViewController extends DisposableController
9296
_showCodeCoverage.value = !_showCodeCoverage.value;
9397
}
9498

99+
void toggleShowProfileInformation() {
100+
_showProfileInformation.value = !_showProfileInformation.value;
101+
}
102+
95103
void clearState() {
96104
// It would be nice to not clear the script history but it is currently
97105
// coupled to ScriptRef objects so that is unsafe.
@@ -157,27 +165,22 @@ class CodeViewController extends DisposableController
157165
scriptsHistory.current.addListener(_scriptHistoryListener);
158166
}
159167

160-
Future<void> refreshCodeCoverage() async {
161-
final hitLines = <int>{};
162-
final missedLines = <int>{};
168+
Future<void> refreshCodeStatistics() async {
163169
final current = parsedScript.value;
164170
if (current == null) {
165171
return;
166172
}
167173
final isolateRef = serviceManager.isolateManager.selectedIsolate.value!;
168-
await _getCoverageReport(
174+
final processedReport = await _getSourceReport(
169175
isolateRef,
170176
current.script,
171-
hitLines,
172-
missedLines,
173177
);
174178

175179
parsedScript.value = ParsedScript(
176180
script: current.script,
177181
highlighter: current.highlighter,
178182
executableLines: current.executableLines,
179-
coverageHitLines: hitLines,
180-
coverageMissedLines: missedLines,
183+
sourceReport: processedReport,
181184
);
182185
}
183186

@@ -211,28 +214,46 @@ class CodeViewController extends DisposableController
211214
_scriptLocation.value = scriptLocation;
212215
}
213216

214-
Future<void> _getCoverageReport(
217+
Future<ProcessedSourceReport> _getSourceReport(
215218
IsolateRef isolateRef,
216-
ScriptRef script,
217-
Set<int> hitLines,
218-
Set<int> missedLines,
219+
Script script,
219220
) async {
221+
final hitLines = <int>{};
222+
final missedLines = <int>{};
220223
try {
221224
final report = await serviceManager.service!.getSourceReport(
222225
isolateRef.id!,
223-
const [SourceReportKind.kCoverage],
226+
// TODO(bkonyi): make _Profile a public report type.
227+
// See https://github.com/dart-lang/sdk/issues/50641
228+
const [
229+
SourceReportKind.kCoverage,
230+
'_Profile',
231+
],
224232
scriptId: script.id!,
225233
reportLines: true,
226234
);
235+
227236
for (final range in report.ranges!) {
228237
final coverage = range.coverage!;
229238
hitLines.addAll(coverage.hits!);
230239
missedLines.addAll(coverage.misses!);
231240
}
241+
242+
final profileReport = report.asProfileReport(script);
243+
return ProcessedSourceReport(
244+
coverageHitLines: hitLines,
245+
coverageMissedLines: missedLines,
246+
profilerEntries:
247+
profileReport.profileRanges.fold<Map<int, ProfileReportEntry>>(
248+
{},
249+
(last, e) => last..addAll(e.entries),
250+
),
251+
);
232252
} catch (e) {
233253
// Ignore - not supported for all vm service implementations.
234254
log('$e');
235255
}
256+
return const ProcessedSourceReport.empty();
236257
}
237258

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

255-
final hitLines = <int>{};
256-
final missedLines = <int>{};
257-
258276
if (script != null) {
259277
final isolateRef = serviceManager.isolateManager.selectedIsolate.value!;
260278
try {
@@ -270,19 +288,16 @@ class CodeViewController extends DisposableController
270288
log('$e');
271289
}
272290

273-
await _getCoverageReport(
291+
final processedReport = await _getSourceReport(
274292
isolateRef,
275293
script,
276-
hitLines,
277-
missedLines,
278294
);
279295

280296
parsedScript.value = ParsedScript(
281297
script: script,
282298
highlighter: highlighter,
283299
executableLines: executableLines,
284-
coverageHitLines: hitLines,
285-
coverageMissedLines: missedLines,
300+
sourceReport: processedReport,
286301
);
287302
}
288303
}
@@ -336,6 +351,23 @@ class CodeViewController extends DisposableController
336351
}
337352
}
338353

354+
class ProcessedSourceReport {
355+
ProcessedSourceReport({
356+
required this.coverageHitLines,
357+
required this.coverageMissedLines,
358+
required this.profilerEntries,
359+
});
360+
361+
const ProcessedSourceReport.empty()
362+
: coverageHitLines = const <int>{},
363+
coverageMissedLines = const <int>{},
364+
profilerEntries = const <int, ProfileReportEntry>{};
365+
366+
final Set<int> coverageHitLines;
367+
final Set<int> coverageMissedLines;
368+
final Map<int, ProfileReportEntry> profilerEntries;
369+
}
370+
339371
/// Maintains the navigation history of the debugger's code area - which files
340372
/// were opened, whether it's possible to navigate forwards and backwards in the
341373
/// history, ...
@@ -368,8 +400,7 @@ class ParsedScript {
368400
required this.script,
369401
required this.highlighter,
370402
required this.executableLines,
371-
required this.coverageHitLines,
372-
required this.coverageMissedLines,
403+
required this.sourceReport,
373404
}) : lines = (script.source?.split('\n') ?? const []).toList();
374405

375406
final Script script;
@@ -378,8 +409,7 @@ class ParsedScript {
378409

379410
final Set<int> executableLines;
380411

381-
final Set<int> coverageHitLines;
382-
final Set<int> coverageMissedLines;
412+
final ProcessedSourceReport? sourceReport;
383413

384414
final List<String> lines;
385415

packages/devtools_app/lib/src/screens/debugger/controls.dart

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import 'debugger_controller.dart';
1919
class DebuggingControls extends StatefulWidget {
2020
const DebuggingControls({Key? key}) : super(key: key);
2121

22-
static const minWidthBeforeScaling = 1600.0;
22+
static const minWidthBeforeScaling = 1750.0;
2323

2424
@override
2525
_DebuggingControlsState createState() => _DebuggingControlsState();
@@ -57,7 +57,7 @@ class _DebuggingControlsState extends State<DebuggingControls>
5757
BreakOnExceptionsControl(controller: controller),
5858
if (isVmApp) ...[
5959
const SizedBox(width: denseSpacing),
60-
CodeCoverageToggle(controller: controller),
60+
CodeStatisticsControls(controller: controller),
6161
],
6262
const Expanded(child: SizedBox(width: denseSpacing)),
6363
_librariesButton(),
@@ -144,8 +144,8 @@ class _DebuggingControlsState extends State<DebuggingControls>
144144
}
145145
}
146146

147-
class CodeCoverageToggle extends StatelessWidget {
148-
const CodeCoverageToggle({
147+
class CodeStatisticsControls extends StatelessWidget {
148+
const CodeStatisticsControls({
149149
super.key,
150150
required this.controller,
151151
});
@@ -155,18 +155,19 @@ class CodeCoverageToggle extends StatelessWidget {
155155
@override
156156
Widget build(BuildContext context) {
157157
return RoundedOutlinedBorder(
158-
child: ValueListenableBuilder<bool>(
159-
valueListenable: controller.codeViewController.showCodeCoverage,
160-
builder: (context, selected, _) {
161-
final isInSmallMode = MediaQuery.of(context).size.width <
158+
child: DualValueListenableBuilder<bool, bool>(
159+
firstListenable: controller.codeViewController.showCodeCoverage,
160+
secondListenable: controller.codeViewController.showProfileInformation,
161+
builder: (context, showCodeCoverage, showProfileInformation, _) {
162+
final isInSmallMode = MediaQuery.of(context).size.width <=
162163
DebuggingControls.minWidthBeforeScaling;
163164
return Row(
164165
children: [
165166
ToggleButton(
166167
label: isInSmallMode ? null : 'Show Coverage',
167168
message: 'Show code coverage',
168169
icon: Codicons.checklist,
169-
isSelected: selected,
170+
isSelected: showCodeCoverage,
170171
outlined: false,
171172
shape: const RoundedRectangleBorder(
172173
borderRadius: BorderRadius.only(
@@ -176,13 +177,25 @@ class CodeCoverageToggle extends StatelessWidget {
176177
),
177178
onPressed: controller.codeViewController.toggleShowCodeCoverage,
178179
),
180+
LeftBorder(
181+
child: ToggleButton(
182+
label: isInSmallMode ? null : 'Show Profile',
183+
message: 'Show profiler hits',
184+
icon: Codicons.flame,
185+
isSelected: showProfileInformation,
186+
outlined: false,
187+
shape: const ContinuousRectangleBorder(),
188+
onPressed: controller
189+
.codeViewController.toggleShowProfileInformation,
190+
),
191+
),
179192
LeftBorder(
180193
child: IconLabelButton(
181194
label: '',
182-
tooltip: 'Refresh code coverage statistics',
195+
tooltip: 'Refresh statistics',
183196
outlined: false,
184-
onPressed: selected
185-
? controller.codeViewController.refreshCodeCoverage
197+
onPressed: showCodeCoverage || showProfileInformation
198+
? controller.codeViewController.refreshCodeStatistics
186199
: null,
187200
minScreenWidthForTextBeforeScaling: 20000,
188201
icon: Icons.refresh,

packages/devtools_app/lib/src/screens/profiler/cpu_profiler.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,10 @@ class _CpuProfilerState extends State<CpuProfiler>
178178
isFilterActive: widget.controller.isToggleFilterActive,
179179
),
180180
const SizedBox(width: denseSpacing),
181-
if (currentTab.key != CpuProfiler.flameChartTab)
181+
if (currentTab.key != CpuProfiler.flameChartTab) ...[
182182
const DisplayTreeGuidelinesToggle(),
183-
const SizedBox(width: denseSpacing),
183+
const SizedBox(width: denseSpacing),
184+
],
184185
UserTagDropdown(widget.controller),
185186
const SizedBox(width: denseSpacing),
186187
ValueListenableBuilder<bool>(

packages/devtools_app/lib/src/screens/vm_developer/vm_service_private_extensions.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,92 @@ extension CpuSamplesPrivateView on CpuSamples {
484484
return _codes!;
485485
}
486486
}
487+
488+
extension ProfileDataRanges on SourceReport {
489+
ProfileReport asProfileReport(Script script) =>
490+
ProfileReport._fromJson(script, json!);
491+
}
492+
493+
class ProfileReportMetaData {
494+
const ProfileReportMetaData._({required this.sampleCount});
495+
final int sampleCount;
496+
}
497+
498+
/// Profiling information for a given line in a [Script].
499+
class ProfileReportEntry {
500+
const ProfileReportEntry({
501+
required this.sampleCount,
502+
required this.line,
503+
required this.inclusive,
504+
required this.exclusive,
505+
});
506+
507+
final int sampleCount;
508+
final int line;
509+
final int inclusive;
510+
final int exclusive;
511+
512+
double get inclusivePercentage => inclusive * 100 / sampleCount;
513+
double get exclusivePercentage => exclusive * 100 / sampleCount;
514+
}
515+
516+
/// Profiling information for a range of token positions in a [Script].
517+
class ProfileReportRange {
518+
ProfileReportRange._fromJson(Script script, Map<String, dynamic> json)
519+
: metadata = ProfileReportMetaData._(
520+
sampleCount: json[_kProfileKey][_kMetadataKey][_kSampleCountKey],
521+
),
522+
inclusiveTicks = json[_kProfileKey][_kInclusiveTicksKey].cast<int>(),
523+
exclusiveTicks = json[_kProfileKey][_kExclusiveTicksKey].cast<int>(),
524+
lines = json[_kProfileKey][_kPositionsKey]
525+
.map<int>(
526+
// It's possible to get a synthetic token position which will
527+
// either be a negative value or a String (e.g., 'ParallelMove'
528+
// or 'NoSource'). We'll just use -1 as a placeholder since we
529+
// won't display anything for these tokens anyway.
530+
(e) => e is int
531+
? script.getLineNumberFromTokenPos(e) ?? _kNoSourcePosition
532+
: _kNoSourcePosition,
533+
)
534+
.toList() {
535+
for (int i = 0; i < lines.length; ++i) {
536+
final line = lines[i];
537+
entries[line] = ProfileReportEntry(
538+
sampleCount: metadata.sampleCount,
539+
line: line,
540+
inclusive: inclusiveTicks[i],
541+
exclusive: exclusiveTicks[i],
542+
);
543+
}
544+
}
545+
546+
static const _kProfileKey = 'profile';
547+
static const _kMetadataKey = 'metadata';
548+
static const _kSampleCountKey = 'sampleCount';
549+
static const _kInclusiveTicksKey = 'inclusiveTicks';
550+
static const _kExclusiveTicksKey = 'exclusiveTicks';
551+
static const _kPositionsKey = 'positions';
552+
static const _kNoSourcePosition = -1;
553+
554+
final ProfileReportMetaData metadata;
555+
final entries = <int, ProfileReportEntry>{};
556+
List<int> inclusiveTicks;
557+
List<int> exclusiveTicks;
558+
List<int> lines;
559+
}
560+
561+
/// A representation of the `_Profile` [SourceReport], which contains profiling
562+
/// information for a given [Script].
563+
class ProfileReport {
564+
ProfileReport._fromJson(Script script, Map<String, dynamic> json)
565+
: _profileRanges = (json['ranges'] as List)
566+
.cast<Map<String, dynamic>>()
567+
.where((e) => e.containsKey('profile'))
568+
.map<ProfileReportRange>(
569+
(e) => ProfileReportRange._fromJson(script, e),
570+
)
571+
.toList();
572+
573+
List<ProfileReportRange> get profileRanges => _profileRanges;
574+
final List<ProfileReportRange> _profileRanges;
575+
}

packages/devtools_app/lib/src/shared/common_widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const debuggerDeviceWidth = 800.0;
3434

3535
const defaultDialogRadius = 20.0;
3636
double get areaPaneHeaderHeight => scaleByFontFactor(36.0);
37+
double get assumedMonospaceCharacterWidth => scaleByFontFactor(9.0);
3738

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

0 commit comments

Comments
 (0)