Skip to content

Commit 1574e86

Browse files
jensjohaCommit Queue
authored andcommitted
[CFE] Add instrumenter that can produce flame graph output
Could for instance be run like this: out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/_fasta/compile.dart out/ReleaseX64/dart pkg/front_end/tool/_fasta/compile.dart.dill.instrumented.dill --omit-platform pkg/front_end/tool/_fasta/compile.dart out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/_fasta/compile.dart --candidates=cfe_compile_trace_candidates.txt out/ReleaseX64/dart pkg/front_end/tool/_fasta/compile.dart.dill.instrumented.dill --omit-platform pkg/front_end/tool/_fasta/compile.dart out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/_fasta/compile.dart --candidates=cfe_compile_trace_candidates_subsequent.txt out/ReleaseX64/dart pkg/front_end/tool/_fasta/compile.dart.dill.instrumented.dill --omit-platform pkg/front_end/tool/_fasta/compile.dart * The first command compiles and instruments "compile.dart" to record all procedure calls. * The second command runs the instrumented compiler on "compile.dart" (but it could have compiled anything). Because every procedure is instrumented this will take a lot longer than a usual run. This produces a trace "cfe_compile_trace.txt" with every call taking at least 1000 microseconds. This can be displayed via Chromes about://tracing tool. It also produces a file "cfe_compile_trace_candidates.txt" with a map of the procedures that on average took at least 500 microseconds. * The third command compiles and instruments "compile.dart", this time only instrumenting the procedures mentioned in the map "cfe_compile_trace_candidates.txt". * The forth command runs the instrumented compiler on "compile.dart". This run shouldn't take significantly longer than a non-instrumented run. It produces a new "cfe_compile_trace.txt" which this time is not filtered. It also produces a file "cfe_compile_trace_candidates_subsequent.txt" of recorded procedures that on average took at least 50 microseconds. * The fifth and sixth commands repeats the third and forth but instrumenting only the procedures mention in "cfe_compile_trace_candidates_subsequent.txt". The third iteration might not be needed, but if the first run was on a smaller input (which it isn't in this example) there might be some calls that on average took long enough to be included in the candidate list because the first call was slow and there were only very few of them, making the second trace very big because there are now a lot of - as it turns out - very quick calls recorded. Adding the third iteration will filter (at least some of) those out. Change-Id: I702c5c9142e525502b02f37744fcdc9d2b0f9b20 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/296902 Reviewed-by: Johnni Winther <[email protected]> Commit-Queue: Jens Johansen <[email protected]>
1 parent b9b1fdf commit 1574e86

File tree

6 files changed

+405
-6
lines changed

6 files changed

+405
-6
lines changed

pkg/front_end/lib/src/base/processed_options.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ class ProcessedOptions {
691691

692692
// Check for $cwd/.dart_tool/package_config.json
693693
Uri? candidate = await checkInDir(dir);
694-
if (candidate != null) return createPackagesFromFile(candidate);
694+
if (candidate != null) return await createPackagesFromFile(candidate);
695695

696696
// Check for cwd(/..)+/.dart_tool/package_config.json
697697
Uri parentDir = dir.resolve('..');
@@ -702,7 +702,7 @@ class ProcessedOptions {
702702
parentDir = dir.resolve('..');
703703
}
704704

705-
if (candidate != null) return createPackagesFromFile(candidate);
705+
if (candidate != null) return await createPackagesFromFile(candidate);
706706
return PackageConfig.empty;
707707
}
708708

pkg/front_end/lib/src/fasta/kernel/kernel_target.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ class KernelTarget extends TargetImplementation {
348348
"Needed precompilations have already been computed.");
349349
_hasComputedNeededPrecompilations = true;
350350
if (loader.roots.isEmpty) return null;
351-
return withCrashReporting<NeededPrecompilations?>(() async {
351+
return await withCrashReporting<NeededPrecompilations?>(() async {
352352
benchmarker?.enterPhase(BenchmarkPhases.outline_kernelBuildOutlines);
353353
await loader.buildOutlines();
354354

@@ -403,7 +403,7 @@ class KernelTarget extends TargetImplementation {
403403

404404
Future<BuildResult> buildOutlines({CanonicalName? nameRoot}) async {
405405
if (loader.roots.isEmpty) return new BuildResult();
406-
return withCrashReporting<BuildResult>(() async {
406+
return await withCrashReporting<BuildResult>(() async {
407407
if (!_hasComputedNeededPrecompilations) {
408408
NeededPrecompilations? neededPrecompilations =
409409
await computeNeededPrecompilations();
@@ -586,7 +586,7 @@ class KernelTarget extends TargetImplementation {
586586
if (loader.roots.isEmpty) {
587587
return new BuildResult(macroApplications: macroApplications);
588588
}
589-
return withCrashReporting<BuildResult>(() async {
589+
return await withCrashReporting<BuildResult>(() async {
590590
ticker.logMs("Building component");
591591

592592
if (macroApplications != null) {

pkg/front_end/test/spell_checking_list_tests.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ dskip
239239
dumping
240240
dumps
241241
dupe
242+
dur
242243
durations
243244
dw
244245
e's
@@ -374,6 +375,7 @@ inlinable
374375
inlineable
375376
insights
376377
instrument
378+
instrumenter
377379
insufficient
378380
intdiv
379381
interactions
@@ -513,7 +515,9 @@ periodically
513515
perm
514516
perms
515517
person
518+
ph
516519
phrase
520+
pid
517521
pink
518522
placement
519523
planned

pkg/front_end/tool/_fasta/compile.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
import 'entry_points.dart' show compileEntryPoint;
66

7-
void main(List<String> arguments) => compileEntryPoint(arguments);
7+
Future<void> main(List<String> arguments) async =>
8+
await compileEntryPoint(arguments);
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:_fe_analyzer_shared/src/util/options.dart';
8+
import 'package:front_end/src/base/processed_options.dart';
9+
10+
import 'package:kernel/binary/ast_from_binary.dart';
11+
import 'package:kernel/kernel.dart';
12+
13+
import '../_fasta/additional_targets.dart';
14+
import '../_fasta/command_line.dart';
15+
import '../_fasta/compile.dart' as fasta_compile;
16+
17+
Future<void> main(List<String> arguments) async {
18+
Directory tmpDir = Directory.systemTemp.createTempSync("cfe_instrumenter");
19+
try {
20+
await _main(arguments, tmpDir);
21+
} finally {
22+
tmpDir.deleteSync(recursive: true);
23+
}
24+
}
25+
26+
Future<void> _main(List<String> inputArguments, Directory tmpDir) async {
27+
List<String> candidates = [];
28+
List<String> arguments = [];
29+
for (String arg in inputArguments) {
30+
if (arg.startsWith("--candidates=")) {
31+
candidates.add(arg.substring("--candidates=".length));
32+
} else {
33+
arguments.add(arg);
34+
}
35+
}
36+
bool reportCandidates = candidates.isEmpty;
37+
setupWantedMap(candidates);
38+
39+
installAdditionalTargets();
40+
ParsedOptions parsedOptions =
41+
ParsedOptions.parse(arguments, optionSpecification);
42+
ProcessedOptions options = analyzeCommandLine("compile", parsedOptions, true);
43+
Uri? output = options.output;
44+
if (output == null) throw "No output";
45+
if (!output.isScheme("file")) throw "Output won't be saved";
46+
47+
print("Compiling the instrumentation library.");
48+
Uri instrumentationLibDill = tmpDir.uri.resolve("instrumenter.dill");
49+
await fasta_compile.main([
50+
"--omit-platform",
51+
"-o=${instrumentationLibDill.toFilePath()}",
52+
Platform.script.resolve("instrumenter_lib.dart").toFilePath()
53+
]);
54+
if (!File.fromUri(instrumentationLibDill).existsSync()) {
55+
throw "Instrumentation library didn't compile as expected.";
56+
}
57+
58+
print("Compiling the given input.");
59+
await fasta_compile.main(arguments);
60+
61+
print("Reading the compiled dill.");
62+
Component component = new Component();
63+
List<int> bytes = new File.fromUri(output).readAsBytesSync();
64+
new BinaryBuilder(bytes).readComponent(component);
65+
66+
List<Procedure> procedures = [];
67+
for (Library lib in component.libraries) {
68+
if (lib.importUri.scheme == "dart") continue;
69+
for (Class c in lib.classes) {
70+
addIfWanted(procedures, c.procedures, includeAll: reportCandidates);
71+
}
72+
addIfWanted(procedures, lib.procedures, includeAll: reportCandidates);
73+
}
74+
print("Procedures: ${procedures.length}");
75+
76+
bytes = File.fromUri(instrumentationLibDill).readAsBytesSync();
77+
new BinaryBuilder(bytes).readComponent(component);
78+
79+
// TODO: Check that this is true.
80+
Library instrumenterLib = component.libraries.last;
81+
Procedure instrumenterInitialize = instrumenterLib.procedures[0];
82+
Procedure instrumenterEnter = instrumenterLib.procedures[1];
83+
Procedure instrumenterExit = instrumenterLib.procedures[2];
84+
Procedure instrumenterReport = instrumenterLib.procedures[3];
85+
86+
int id = 0;
87+
for (Procedure p in procedures) {
88+
int thisId = id++;
89+
wrapProcedure(p, thisId, instrumenterEnter, instrumenterExit);
90+
}
91+
92+
initializeAndReport(component.mainMethod!, instrumenterInitialize, procedures,
93+
instrumenterReport, reportCandidates);
94+
95+
print("Writing output.");
96+
String outString = output.toFilePath() + ".instrumented.dill";
97+
await writeComponentToBinary(component, outString);
98+
print("Wrote to $outString");
99+
}
100+
101+
void addIfWanted(List<Procedure> output, List<Procedure> input,
102+
{required bool includeAll}) {
103+
for (Procedure p in input) {
104+
if (p.function.body == null) continue;
105+
// Yielding functions doesn't work well with the begin/end scheme.
106+
if (p.function.dartAsyncMarker == AsyncMarker.SyncStar) continue;
107+
if (p.function.dartAsyncMarker == AsyncMarker.AsyncStar) continue;
108+
if (!includeAll) {
109+
String name = getName(p);
110+
Set<String> procedureNamesWantedInFile =
111+
wanted[p.fileUri.pathSegments.last] ?? const {};
112+
if (!procedureNamesWantedInFile.contains(name)) {
113+
continue;
114+
}
115+
}
116+
output.add(p);
117+
}
118+
}
119+
120+
String getName(Procedure p) {
121+
if (p.parent is Class) {
122+
return "${(p.parent as Class).name}.${p.name.text}";
123+
} else {
124+
return p.name.text;
125+
}
126+
}
127+
128+
void setupWantedMap(List<String> candidates) {
129+
for (String filename in candidates) {
130+
File f = new File(filename);
131+
if (!f.existsSync()) throw "$filename doesn't exist.";
132+
for (String line in f.readAsLinesSync()) {
133+
String file = line.substring(0, line.indexOf("|"));
134+
String displayName = line.substring(line.indexOf("|") + 1);
135+
Set<String> existingInFile = wanted[file] ??= {};
136+
existingInFile.add(displayName);
137+
}
138+
}
139+
}
140+
141+
Map<String, Set<String>> wanted = {};
142+
143+
void initializeAndReport(
144+
Procedure mainProcedure,
145+
Procedure initializeProcedure,
146+
List<Procedure> procedures,
147+
Procedure instrumenterReport,
148+
bool reportCandidates) {
149+
ChildReplacer childReplacer;
150+
try {
151+
childReplacer = getBody(mainProcedure.function);
152+
} catch (e) {
153+
throw "$mainProcedure: $e";
154+
}
155+
156+
Block block = new Block([
157+
new ExpressionStatement(new StaticInvocation(initializeProcedure,
158+
new Arguments([new IntLiteral(procedures.length)]))),
159+
new TryFinally(
160+
childReplacer.originalChild as Statement,
161+
new ExpressionStatement(new StaticInvocation(
162+
instrumenterReport,
163+
new Arguments([
164+
new ListLiteral(procedures
165+
.map((p) => new StringLiteral(
166+
"${p.fileUri.pathSegments.last}|${getName(p)}"))
167+
.toList()),
168+
new BoolLiteral(reportCandidates),
169+
])))),
170+
]);
171+
childReplacer.replacer(block);
172+
}
173+
174+
class ChildReplacer {
175+
final void Function(TreeNode replacement) replacer;
176+
final TreeNode originalChild;
177+
178+
ChildReplacer(this.replacer, this.originalChild);
179+
}
180+
181+
ChildReplacer getBody(FunctionNode functionNode) {
182+
// Is this an originally non-sync, but now transformed method?
183+
if (functionNode.dartAsyncMarker != AsyncMarker.Sync &&
184+
functionNode.dartAsyncMarker != functionNode.asyncMarker) {
185+
if (functionNode.dartAsyncMarker == AsyncMarker.Async) {
186+
// It was originally an async method. (this will work for the VM async
187+
// transformation).
188+
Block block = functionNode.body as Block;
189+
FunctionDeclaration functionDeclaration = block.statements
190+
.firstWhere((s) => s is FunctionDeclaration) as FunctionDeclaration;
191+
TryCatch tryCatch = functionDeclaration.function.body as TryCatch;
192+
Block tryCatchBlock = tryCatch.body as Block;
193+
LabeledStatement labeledStatement =
194+
tryCatchBlock.statements[0] as LabeledStatement;
195+
Block labeledStatementBlock = labeledStatement.body as Block;
196+
return new ChildReplacer((TreeNode newChild) {
197+
Block newBlock = new Block([newChild as Statement]);
198+
labeledStatement.body = newBlock;
199+
newBlock.parent = labeledStatement;
200+
}, labeledStatementBlock);
201+
} else if (functionNode.dartAsyncMarker == AsyncMarker.SyncStar) {
202+
// It was originally a sync* method. This will work for the VM
203+
// transformation.
204+
Block block = functionNode.body as Block;
205+
FunctionDeclaration functionDeclaration = block.statements
206+
.firstWhere((s) => s is FunctionDeclaration) as FunctionDeclaration;
207+
Block functionDeclarationBlock =
208+
functionDeclaration.function.body as Block;
209+
Block nestedBlock = functionDeclarationBlock.statements[0] as Block;
210+
return new ChildReplacer((TreeNode newChild) {
211+
functionDeclarationBlock.statements[0] = newChild as Statement;
212+
newChild.parent = functionDeclarationBlock;
213+
}, nestedBlock);
214+
} else {
215+
throw "Unsupported: ${functionNode.dartAsyncMarker}: "
216+
"${functionNode.body}";
217+
}
218+
} else {
219+
// Should be a regular sync method.
220+
assert(functionNode.dartAsyncMarker == AsyncMarker.Sync);
221+
assert(functionNode.asyncMarker == AsyncMarker.Sync);
222+
return new ChildReplacer((TreeNode newChild) {
223+
functionNode.body = newChild as Statement;
224+
newChild.parent = functionNode;
225+
}, functionNode.body as TreeNode);
226+
}
227+
}
228+
229+
void wrapProcedure(Procedure p, int id, Procedure instrumenterEnter,
230+
Procedure instrumenterExit) {
231+
ChildReplacer childReplacer;
232+
try {
233+
childReplacer = getBody(p.function);
234+
} catch (e) {
235+
throw "$p: $e";
236+
}
237+
238+
Block block = new Block([
239+
new ExpressionStatement(new StaticInvocation(
240+
instrumenterEnter, new Arguments([new IntLiteral(id)]))),
241+
childReplacer.originalChild as Statement
242+
]);
243+
TryFinally tryFinally = new TryFinally(
244+
block,
245+
new ExpressionStatement(new StaticInvocation(
246+
instrumenterExit, new Arguments([new IntLiteral(id)]))));
247+
childReplacer.replacer(tryFinally);
248+
}

0 commit comments

Comments
 (0)