Skip to content

Commit b2766ec

Browse files
mkustermannCommit Queue
authored andcommitted
[dart2wasm] Add self-compile dart2wasm benchmark.
We make the core of dart2wasm independent of dart:io and add a `pkg/dart2wasm/benchmark/self_compile_benchmark.dart` file. We can compile it with * `dart compile exe` and measure * `dart compile wasm` and masure on various JS engines The current results (assuming I measured correctly) are: | Time | Memory ----------------------- VM AOT | 32s | 0.8 GB ----------------------- D8 | 32s | 1.3 GB ---------------------- JSShell | 87s | 2.8 GB ----------------------- JSC | 70s | 4.4 GB Measured via something like ``` % dart compile exe -o selfcompile.exe \ pkg/dart2wasm/benchmark/self_compile_benchmark.dart % dart compile wasm --no-minify --no-strip-wasm -O2 \ -o selfcompile.wasm \ pkg/dart2wasm/benchmark/self_compile_benchmark.dart % alias measure="/usr/bin/time -f '%M peak KB, %e seconds'" % measure ./selfcompile.exe % measure pkg/dart2wasm/tool/run_benchmark --d8 selfcompile.wasm % measure pkg/dart2wasm/tool/run_benchmark --jsc selfcompile.wasm % measure pkg/dart2wasm/tool/run_benchmark --jsshell selfcompile.wasm ``` The added test can be run via ``` % python3 tools/test.py -n unittest-mac pkg/dart2wasm/test/self_compile_test ... ``` Change-Id: I9a3eeb5e8a7867f0e3ec7729cec57f10da221ae5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/442860 Reviewed-by: Ömer Ağacan <[email protected]> Commit-Queue: Martin Kustermann <[email protected]>
1 parent d417da5 commit b2766ec

File tree

8 files changed

+359
-14
lines changed

8 files changed

+359
-14
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) 2025, 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:convert';
6+
import 'dart:io' show FileSystemException;
7+
import 'dart:typed_data';
8+
9+
import 'package:front_end/src/api_prototype/file_system.dart'
10+
show FileSystem, FileSystemEntity;
11+
12+
final _fakeSdkRoot = '/FakeSdkRoot';
13+
14+
abstract class WasmCompilerFileSystemBase implements FileSystem {
15+
final String sdkRoot = _fakeSdkRoot;
16+
17+
@override
18+
FileSystemEntity entityForUri(Uri uri) =>
19+
WasmCompilerFileSystemEntity(this, uri);
20+
21+
Future<String?> _tryReadString(Uri uri) async {
22+
final bytes = await _tryReadBytes(uri);
23+
if (bytes == null) return null;
24+
return utf8.decode(bytes);
25+
}
26+
27+
Future<Uint8List?> _tryReadBytes(Uri uri) async {
28+
String relativePath;
29+
final path = uri.path;
30+
if (path.startsWith('/')) {
31+
assert(uri.path.startsWith(_fakeSdkRoot));
32+
relativePath = uri.path.substring(_fakeSdkRoot.length + 1);
33+
} else {
34+
relativePath = path;
35+
}
36+
37+
return tryReadBytesSync(relativePath);
38+
}
39+
40+
void writeStringSync(String filename, String value) {
41+
writeBytesSync(filename, utf8.encode(value));
42+
}
43+
44+
// Overriden by subclasses.
45+
Uint8List? tryReadBytesSync(String relativePath);
46+
47+
// Overriden by subclasses.
48+
void writeBytesSync(String filename, Uint8List bytes);
49+
}
50+
51+
class WasmCompilerFileSystemEntity implements FileSystemEntity {
52+
final WasmCompilerFileSystemBase _fileSystem;
53+
54+
@override
55+
final Uri uri;
56+
57+
WasmCompilerFileSystemEntity(this._fileSystem, this.uri);
58+
59+
@override
60+
int get hashCode => Object.hash(_fileSystem, uri);
61+
62+
@override
63+
bool operator ==(Object other) =>
64+
other is WasmCompilerFileSystemEntity &&
65+
other._fileSystem == _fileSystem &&
66+
other.uri == uri;
67+
68+
@override
69+
Future<bool> exists() async {
70+
// CFE uses it to detect whether a directory exists.
71+
// We claim it does (because we cannot use JS APIs to detect existence of
72+
// directory).
73+
if (uri.path.endsWith('/')) return true;
74+
return (await _fileSystem._tryReadBytes(uri)) != null;
75+
}
76+
77+
@override
78+
Future<bool> existsAsyncIfPossible() => exists();
79+
80+
@override
81+
Future<Uint8List> readAsBytes() async {
82+
final result = await _fileSystem._tryReadBytes(uri);
83+
if (result != null) return result;
84+
throw FileSystemException('Failed to read $uri');
85+
}
86+
87+
@override
88+
Future<Uint8List> readAsBytesAsyncIfPossible() => readAsBytes();
89+
90+
@override
91+
Future<String> readAsString() async {
92+
final result = await _fileSystem._tryReadString(uri);
93+
if (result != null) return result;
94+
throw FileSystemException('Failed to read $uri');
95+
}
96+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) 2025, 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+
import 'dart:typed_data';
7+
8+
import 'filesystem_base.dart';
9+
10+
class WasmCompilerFileSystem extends WasmCompilerFileSystemBase {
11+
@override
12+
Uint8List? tryReadBytesSync(String relativePath) {
13+
try {
14+
return File(relativePath).readAsBytesSync();
15+
} catch (_) {
16+
print('-> failed to load $relativePath');
17+
return null;
18+
}
19+
}
20+
21+
@override
22+
void writeBytesSync(String filename, Uint8List bytes) {
23+
File(filename).writeAsBytesSync(bytes);
24+
}
25+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2025, 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:js_interop';
6+
import 'dart:typed_data';
7+
8+
import 'filesystem_base.dart';
9+
10+
class WasmCompilerFileSystem extends WasmCompilerFileSystemBase {
11+
@override
12+
Uint8List? tryReadBytesSync(String path) {
13+
try {
14+
if (isD8) {
15+
return d8Read(path).toDart.asUint8List();
16+
}
17+
if (isJSC) {
18+
path = path.startsWith('/') ? path : '../../$path';
19+
return jscRead(path, 'binary').toDart;
20+
}
21+
if (isJSShell) {
22+
return jsshellRead(path, 'binary').toDart;
23+
}
24+
throw 'Unknown JS Shell';
25+
} catch (_) {
26+
print('-> failed to load $path');
27+
return null;
28+
}
29+
}
30+
31+
@override
32+
void writeBytesSync(String path, Uint8List bytes) {
33+
try {
34+
if (isJSShell) {
35+
return jsshellWrite(path, bytes.toJS);
36+
}
37+
final buffer =
38+
(Uint8List(bytes.length)..setRange(0, bytes.length, bytes)).buffer;
39+
if (isD8) {
40+
return d8Write(path, buffer.toJS);
41+
}
42+
if (isJSC) {
43+
path = path.startsWith('/') ? path : '../../$path';
44+
return jscWrite(path, buffer.toJS);
45+
}
46+
throw 'Unknown JS Shell';
47+
} catch (_) {
48+
print('-> failed to write to $path');
49+
}
50+
}
51+
}
52+
53+
final bool isD8 = d8Only != null;
54+
final bool isJSC = !isD8 && notD8JSCOnly != null;
55+
final bool isJSShell = !isD8 && !isJSC;
56+
57+
@JS('readbuffer')
58+
external JSArrayBuffer d8Read(String filename);
59+
60+
@JS('writeFile')
61+
external void d8Write(String filename, JSArrayBuffer bytes);
62+
63+
@JS('readFile') // Have to prepend ../../ for relative paths
64+
external JSUint8Array jscRead(String filename, String binary);
65+
66+
@JS('writeFile') // Have to prepend ../../ for relative paths
67+
external void jscWrite(String filename, JSArrayBuffer bytes);
68+
69+
@JS('os.file.readFile')
70+
external JSUint8Array jsshellRead(String filename, String binary);
71+
72+
@JS('os.file.writeTypedArrayToFile')
73+
external void jsshellWrite(String filename, JSUint8Array bytes);
74+
75+
@JS('readbuffer')
76+
external JSAny? d8Only;
77+
78+
@JS('readFile')
79+
external JSAny? notD8JSCOnly;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2025, 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 'package:_fe_analyzer_shared/src/util/colors.dart' as colors;
6+
import 'package:dart2wasm/compile.dart';
7+
import 'package:dart2wasm/compiler_options.dart';
8+
9+
import 'filesystem_io.dart' if (dart.library.js_interop) 'filesystem_js.dart';
10+
11+
Future main(List<String> args) async {
12+
final sw = Stopwatch()..start();
13+
final fileSystem = WasmCompilerFileSystem();
14+
final result = await compile(
15+
fileSystem, 'pkg/dart2wasm/benchmark/self_compile_benchmark.dart');
16+
print('Dart2WasmSelfCompile(RunTimeRaw): ${sw.elapsed.inMilliseconds} ms.');
17+
18+
if (args.isNotEmpty) {
19+
final module = result.wasmModules.values.single;
20+
final wasmFile = args.single;
21+
fileSystem.writeBytesSync(wasmFile, module.moduleBytes);
22+
}
23+
}
24+
25+
Future<CompilationSuccess> compile(
26+
WasmCompilerFileSystem fileSystem, String mainFile) async {
27+
// Avoid CFE self-detecting whether `stdout`/`stderr` is terminal and supports
28+
// colors (as we don't have `dart:io` available when we run dart2wasm in a
29+
// wasm runtime).
30+
colors.enableColors = false;
31+
32+
final main = Uri.file('${fileSystem.sdkRoot}/$mainFile');
33+
34+
final options = WasmCompilerOptions(mainUri: main, outputFile: 'out.wasm');
35+
options.librariesSpecPath =
36+
Uri.file('${fileSystem.sdkRoot}/sdk/lib/libraries.json');
37+
38+
final result = await compileToModule(
39+
options, fileSystem, (mod) => Uri.parse('$mod.maps'), (diag) {
40+
print('Diagnostics: ${diag.severity} ${diag.plainTextFormatted}');
41+
});
42+
if (result is! CompilationSuccess) {
43+
throw 'Compilation Failed: $result';
44+
}
45+
return result;
46+
}

pkg/dart2wasm/lib/compile.dart

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import 'dart:typed_data';
77

88
import 'package:build_integration/file_system/multi_root.dart'
99
show MultiRootFileSystem, MultiRootFileSystemEntity;
10-
import 'package:front_end/src/api_prototype/standard_file_system.dart'
11-
show StandardFileSystem;
10+
import 'package:front_end/src/api_prototype/file_system.dart' show FileSystem;
1211
import 'package:front_end/src/api_unstable/vm.dart'
1312
show
1413
CompilerOptions,
@@ -21,6 +20,7 @@ import 'package:kernel/class_hierarchy.dart';
2120
import 'package:kernel/core_types.dart';
2221
import 'package:kernel/kernel.dart' show writeComponentToText;
2322
import 'package:kernel/library_index.dart';
23+
import 'package:kernel/text/ast_to_text.dart';
2424
import 'package:kernel/type_environment.dart';
2525
import 'package:kernel/verifier.dart';
2626
import 'package:vm/kernel_front_end.dart' show writeDepfile;
@@ -30,6 +30,7 @@ import 'package:vm/transformations/to_string_transformer.dart'
3030
as to_string_transformer;
3131
import 'package:vm/transformations/type_flow/transformer.dart' as globalTypeFlow
3232
show transformComponent;
33+
import 'package:vm/transformations/type_flow/utils.dart' as tfa_utils;
3334
import 'package:vm/transformations/unreachable_code_elimination.dart'
3435
as unreachable_code_elimination;
3536
import 'package:wasm_builder/wasm_builder.dart' show Serializer;
@@ -64,7 +65,7 @@ class CompilationSuccess extends CompilationResult {
6465
CompilationSuccess(this.wasmModules, this.jsRuntime, this.supportJs);
6566
}
6667

67-
class CompilationError extends CompilationResult {}
68+
abstract class CompilationError extends CompilationResult {}
6869

6970
/// The CFE has crashed with an exception.
7071
///
@@ -74,6 +75,9 @@ class CFECrashError extends CompilationError {
7475
final StackTrace stackTrace;
7576

7677
CFECrashError(this.error, this.stackTrace);
78+
79+
@override
80+
String toString() => 'CFECrashError($error):\n$stackTrace';
7781
}
7882

7983
/// Compiling the Dart program resulted in compile-time errors.
@@ -122,8 +126,10 @@ const List<String> _librariesToIndex = [
122126
/// mappings.
123127
Future<CompilationResult> compileToModule(
124128
compiler.WasmCompilerOptions options,
129+
FileSystem fileSystem,
125130
Uri Function(String moduleName)? sourceMapUrlGenerator,
126-
void Function(DiagnosticMessage) handleDiagnosticMessage) async {
131+
void Function(DiagnosticMessage) handleDiagnosticMessage,
132+
{void Function(String, String)? writeFile}) async {
127133
var hadCompileTimeError = false;
128134
void diagnosticMessageHandler(DiagnosticMessage message) {
129135
if (message.severity == Severity.error) {
@@ -161,12 +167,13 @@ Future<CompilationResult> compileToModule(
161167
}
162168
..explicitExperimentalFlags = options.feExperimentalFlags
163169
..verbose = false
164-
..onDiagnostic = diagnosticMessageHandler;
170+
..onDiagnostic = diagnosticMessageHandler
171+
..fileSystem = fileSystem;
165172
if (options.multiRootScheme != null) {
166173
compilerOptions.fileSystem = MultiRootFileSystem(
167174
options.multiRootScheme!,
168175
options.multiRoots.isEmpty ? [Uri.base] : options.multiRoots,
169-
StandardFileSystem.instance);
176+
compilerOptions.fileSystem);
170177
}
171178

172179
Future<Uri?> resolveUri(Uri? uri) async {
@@ -229,8 +236,8 @@ Future<CompilationResult> compileToModule(
229236
ClassHierarchy(component, coreTypes) as ClosedWorldClassHierarchy;
230237
LibraryIndex libraryIndex = LibraryIndex(component, _librariesToIndex);
231238

232-
if (options.dumpKernelAfterCfe != null) {
233-
writeComponentToText(component, path: options.dumpKernelAfterCfe!);
239+
if (options.dumpKernelAfterCfe != null && writeFile != null) {
240+
writeFile(options.dumpKernelAfterCfe!, writeComponentToString(component));
234241
}
235242

236243
if (options.deleteToStringPackageUri.isNotEmpty) {
@@ -269,8 +276,8 @@ Future<CompilationResult> compileToModule(
269276
isDynamicSubmodule: isDynamicSubmodule);
270277
target.recordClasses = recordClasses;
271278

272-
if (options.dumpKernelBeforeTfa != null) {
273-
writeComponentToText(component, path: options.dumpKernelBeforeTfa!);
279+
if (options.dumpKernelBeforeTfa != null && writeFile != null) {
280+
writeFile(options.dumpKernelBeforeTfa!, writeComponentToString(component));
274281
}
275282

276283
mixin_deduplication.transformLibraries(librariesToTransform);
@@ -313,6 +320,11 @@ Future<CompilationResult> compileToModule(
313320
if (!isDynamicSubmodule) {
314321
_patchMainTearOffs(coreTypes, component);
315322

323+
// We initialize the [printStats] to `false` to prevent it's field
324+
// initializer to run (which only works on VM -- but we want our compiler
325+
// to also run if compiled via dart2js/dart2wasm)
326+
tfa_utils.printStats = false;
327+
316328
// Keep the flags in-sync with
317329
// pkg/vm/test/transformations/type_flow/transformer_test.dart
318330
globalTypeFlow.transformComponent(target, coreTypes, component,
@@ -454,3 +466,9 @@ String _generateSupportJs(TranslatorOptions options) {
454466
];
455467
return '(${requiredFeatures.join('&&')})';
456468
}
469+
470+
String writeComponentToString(Component component) {
471+
final buffer = StringBuffer();
472+
Printer(buffer).writeComponentFile(component);
473+
return '$buffer';
474+
}

pkg/dart2wasm/lib/generate_wasm.dart

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import 'dart:io';
66

7+
import 'package:front_end/src/api_prototype/standard_file_system.dart'
8+
show StandardFileSystem;
79
import 'package:front_end/src/api_unstable/vm.dart' show printDiagnosticMessage;
810
import 'package:path/path.dart' as path;
911

@@ -76,9 +78,12 @@ Future<int> generateWasm(WasmCompilerOptions options,
7678
? moduleNameToRelativeSourceMapUri
7779
: null;
7880

79-
CompilationResult result =
80-
await compileToModule(options, relativeSourceMapUrlMapper, (message) {
81+
CompilationResult result = await compileToModule(
82+
options, StandardFileSystem.instance, relativeSourceMapUrlMapper,
83+
(message) {
8184
if (!options.dryRun) printDiagnosticMessage(message, errorPrinter);
85+
}, writeFile: (String filename, String content) {
86+
File(filename).writeAsStringSync(content);
8287
});
8388

8489
if (result is CompilationDryRunResult) {

0 commit comments

Comments
 (0)