Skip to content

Commit bfae89b

Browse files
davidmorganlrhn
authored andcommitted
[macros] Add benchmark case generator, ToString() macro (#3855)
* [macros] Add ToString() macro. * [macros] Add benchmark case generator.
1 parent fdfa3b6 commit bfae89b

File tree

19 files changed

+562
-31
lines changed

19 files changed

+562
-31
lines changed

working/macros/dart_model/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,48 @@ as a library;\
2020

2121
The "macros" referred to in this exploration are independent of the in-progress
2222
macro implementation, hence the "scare quotes".
23+
24+
## Benchmarks
25+
26+
`testing/benchmark` is a tool to assist in benchmarking, it creates codebases
27+
of the specified size and codegen strategy.
28+
29+
### Scenarios
30+
31+
`trivial_macros` is a scenario with three trivial macros: `Equals()`,
32+
`HashCode()` and `ToString()`. These inspect the fields of a class and generate
33+
corresponding (shallow) `operator==`, `hashCode` and `toString()`.
34+
35+
### Strategies
36+
37+
Four strategies are supported:
38+
39+
`macro` uses the experimental macro implementation from the SDK;\
40+
`dartModel` uses this exploratory macro implementation;\
41+
`manual` writes equivalent code directly in the source;\
42+
`none` means the functionality is missing altogether.
43+
44+
### Example
45+
46+
To compare the SDK macros with this exploratory implementation:
47+
48+
```
49+
$ cd testing/benchmark
50+
51+
# Create a large example using SDK macros: 64 large libraries, about 67k LOC.
52+
$ dart bin/main.dart macros macro 64
53+
# Now open /tmp/dart_model_benchmark/macros in your IDE and modify some files
54+
# to see how the analyzer responds.
55+
56+
# Create an equivalent example using `dart_model`.
57+
$ dart bin/main.dart dartModel dartModel 64
58+
# In a new terminal, launch the "macro" host.
59+
$ cd macro_host
60+
$ dart bin/main.dart /tmp/dart_model_benchmark/dartModel/package_under_test
61+
# In a second new terminal, launch the "macro" process.
62+
$ cd testing/test_macros
63+
$ dart bin/main.dart
64+
# Now open /tmp/dart_model_benchmark/dartModel in your IDE and modify some
65+
# files to see how the analyzer responds; you can watch the macro host terminal
66+
# to see when it is rewriting augmentation files.
67+
```

working/macros/dart_model/dart_model/lib/delta.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ extension type Delta.fromJson(Map<String, Object?> node) {
9393

9494
List<Removal> get removals => (node['removals'] as List).cast();
9595

96+
List<String> get uris => updates
97+
.map((u) => u.path.uri)
98+
.nonNulls
99+
.followedBy(removals.map((r) => r.path.uri).nonNulls)
100+
.toSet()
101+
.toList();
102+
96103
String prettyPrint() => const JsonEncoder.withIndent(' ').convert(node);
97104

98105
void update(Model previous) {

working/macros/dart_model/dart_model/lib/model.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ extension type Path.fromJson(List<Object?> node) {
265265

266266
List<String> get path => (node as List).cast();
267267

268+
String? get uri => path.isEmpty ? null : path.first;
269+
268270
Path followedByOne(String element) =>
269271
Path(path.followedBy([element]).toList());
270272

working/macros/dart_model/dart_model_analyzer_service/lib/dart_model_analyzer_service.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,8 @@ class DartModelAnalyzerService implements Service {
8080
return Stream.fromIterable(<FileSystemEvent?>[null])
8181
.followedBy(changes)
8282
.asyncMap((change) async {
83-
print('File change: $change');
8483
if (change != null) context!.changeFile(change.path);
85-
final changed = await context!.applyPendingFileChanges();
86-
print('Changed: $changed');
84+
await context!.applyPendingFileChanges();
8785
final model = await this.query(query);
8886
var delta = Delta.compute(previousModel, model);
8987
// Round trip to check serialization works.

working/macros/dart_model/macro_client/lib/macro_client.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,23 @@ class MacroClient {
1313

1414
MacroClient(this.arguments);
1515

16-
Future<void> host(List<Macro> macros) async {
17-
final socket = await Socket.connect('localhost', 26199);
16+
Future<void> run(List<Macro> macros) async {
17+
print('~~~ setup');
18+
print('Connect to localhost:26199.');
19+
Socket socket;
20+
try {
21+
socket = await Socket.connect('localhost', 26199);
22+
socket.setOption(SocketOption.tcpNoDelay, true);
23+
} catch (_) {
24+
print('Connection failed! Is `package:macro_host` running?');
25+
exit(1);
26+
}
1827
final host = SocketHost(socket);
19-
for (final macro in macros) {
28+
29+
print('~~~ running macros');
30+
for (var i = 0; i != macros.length; ++i) {
31+
final macro = macros[i];
32+
print('${i + 1}. ${macro.name}');
2033
macro.start(host);
2134
}
2235
}

working/macros/dart_model/macro_host/bin/main.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,16 @@ import 'package:dart_model_analyzer_service/dart_model_analyzer_service.dart';
1010
import 'package:macro_host/macro_host.dart';
1111

1212
Future<void> main(List<String> arguments) async {
13+
if (arguments.length != 1) {
14+
_usage();
15+
}
1316
final workspace = arguments[0];
17+
if (!Directory(workspace).existsSync()) {
18+
_usage();
19+
}
20+
21+
print('~~~ setup');
22+
print('Launching analyzer on: $workspace');
1423
final contextBuilder = ContextBuilder();
1524
final analysisContext = contextBuilder.createContext(
1625
contextRoot:
@@ -22,3 +31,13 @@ Future<void> main(List<String> arguments) async {
2231
return File(path);
2332
}).run();
2433
}
34+
35+
void _usage() {
36+
print('''
37+
Usage: dart bin/main.dart <absolute path to workspace>
38+
39+
Hosts macros in a workspace, so they can react to changes in the workspace
40+
and write updates to augmentations.
41+
''');
42+
exit(1);
43+
}

working/macros/dart_model/macro_host/lib/macro_host.dart

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66
import 'dart:io';
77

8+
import 'package:dart_model/model.dart';
89
import 'package:dart_model/query.dart';
910
import 'package:macro_protocol/host.dart';
1011

@@ -16,47 +17,69 @@ class MacroHost implements Host {
1617
final File? Function(Uri) uriConverter;
1718
ServerSocket? serverSocket;
1819
final Map<String, Map<Object, String>> _augmentationsByUri = {};
20+
final Set<String> _augmentationsToWrite = {};
21+
22+
int _augmentationCounter = 0;
23+
bool _flushing = false;
1924

2025
MacroHost(this.service, this.uriConverter);
2126

2227
Future<void> run() async {
2328
serverSocket = await ServerSocket.bind('localhost', 26199);
24-
print('macro_host listening on localhost:26199');
25-
29+
print('Listening on localhost:26199.');
30+
print('~~~ hosting');
2631
await for (final socket in serverSocket!) {
32+
print('Incoming connection.');
33+
socket.setOption(SocketOption.tcpNoDelay, true);
2734
SocketClient(this, socket);
2835
}
2936
}
3037

31-
void handle(Socket socket) {
32-
print('Got $socket');
33-
}
34-
3538
@override
3639
Future<void> augment(
37-
{required Object macro,
40+
{required QualifiedName macro,
3841
required String uri,
3942
required String augmentation}) async {
40-
print('Augment: $uri $augmentation');
41-
final baseFile = uriConverter(Uri.parse(uri))!;
42-
final baseName =
43-
baseFile.path.substring(baseFile.path.lastIndexOf('/') + 1);
44-
final augmentationFile = File(baseFile.path.replaceAll('.dart', '.a.dart'));
45-
46-
print(_augmentationsByUri);
43+
print(' ${macro.name} --${augmentation.length}--> $uri');
4744
if (_augmentationsByUri[uri] == null) {
48-
print('create map');
49-
_augmentationsByUri[uri] = Map.identity();
45+
_augmentationsByUri[uri] = {};
5046
}
5147
final augmentations = _augmentationsByUri[uri]!;
5248
augmentations[macro] = augmentation;
53-
print(_augmentationsByUri);
49+
_augmentationsToWrite.add(uri);
50+
51+
// Give other augmentations a chance to arrive before flushing.
52+
if (_flushing) return;
53+
++_augmentationCounter;
54+
final augmentationCounter = _augmentationCounter;
55+
unawaited(Future.delayed(Duration(milliseconds: 20)).then<void>((_) async {
56+
if (_augmentationCounter != augmentationCounter) return;
57+
_flushing = true;
58+
while (_augmentationsToWrite.isNotEmpty) {
59+
await flushAugmentations();
60+
}
61+
_flushing = false;
62+
}));
63+
}
5464

55-
// TODO(davidmorgan): write async? Needs locking.
56-
augmentationFile.writeAsStringSync('''
65+
Future<void> flushAugmentations() async {
66+
final augmentationsToWrite = _augmentationsToWrite.toList();
67+
_augmentationsToWrite.clear();
68+
final futures = <Future>[];
69+
for (final uri in augmentationsToWrite) {
70+
final augmentations = _augmentationsByUri[uri]!;
71+
final baseFile = uriConverter(Uri.parse(uri))!;
72+
final baseName =
73+
baseFile.path.substring(baseFile.path.lastIndexOf('/') + 1);
74+
final augmentationFile =
75+
File(baseFile.path.replaceAll('.dart', '.a.dart'));
76+
print('Write: ${augmentationFile.path}');
77+
futures.add(augmentationFile.writeAsString('''
5778
augment library '$baseName';
5879
5980
${augmentations.values.join('\n\n')}
60-
''');
81+
'''));
82+
}
83+
await Future.wait(futures);
6184
}
6285
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2024, 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:benchmark/trivial_macros/input_generator.dart';
8+
import 'package:benchmark/workspace.dart';
9+
10+
Future<void> main(List<String> arguments) async {
11+
if (arguments.length != 3) {
12+
print('''
13+
Creates packages to benchmark macro performance. Usage:
14+
15+
dart bin/main.dart <workspace name> <macro|dartModel|manual|none> <# libraries>
16+
''');
17+
exit(1);
18+
}
19+
20+
final workspaceName = arguments[0];
21+
final strategy = Strategy.values.where((e) => e.name == arguments[1]).single;
22+
final libraryCount = int.parse(arguments[2]);
23+
24+
print('Creating under: /tmp/dart_model_benchmark/$workspaceName');
25+
final workspace = Workspace(workspaceName);
26+
final inputGenerator = TrivialMacrosInputGenerator(
27+
fieldsPerClass: 100,
28+
classesPerLibrary: 10,
29+
librariesPerCycle: libraryCount,
30+
strategy: strategy);
31+
inputGenerator.generate(workspace);
32+
await workspace.pubGet();
33+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) 2024, 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:math';
6+
7+
final _random = Random.secure();
8+
int get largeRandom =>
9+
_random.nextInt(0xFFFFFFFF) + (_random.nextInt(0x7FFFFFFF) << 32);

0 commit comments

Comments
 (0)