Skip to content

Commit f89332a

Browse files
authored
Batch writes (#3771)
* Use batches for builds * Add changelog entry to build runner * Review feedback aAnother attempt for #3321, my [previous PR](#3418) for this got stale, this one is a bit simpler. This attempts to solve the issue of `build_runner` and external tools watching for file system events (like the analysis server) not playing too well together. At the moment, a common issue is that, as a user runs `build_runner build` and the generated assets are written as they are generated, the analysis server will see many file changes and keep re-analyzing sources in an incomplete state. By making `build_runner` cache pending writes in memory and then flushing them after a completed build, we'll likely get all changes to disk before they're picked up by external tools, reducing friction. I've implemented this at a fairly low level (the raw file readers and writers making up the default `IOEnvironment`). They have to opt-in to batches by checking for the presence of a zone key when reading or writing assets. We support batches by default when not in low-resources mode. A batch runs in a zone wrapping a whole build run. A todo is that I need to write integration tests for this, but it would be good to get a review for the overall approach before completing this. Closes #3321. * Bump version of build_runner_core * Make batch private, fix dependency resolution * Start fixing tests * Add lints dependency for out-of-workspace pkg
1 parent 039aeca commit f89332a

19 files changed

+348
-21
lines changed

_test_common/lib/in_memory_writer.dart

+3
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@ class InMemoryRunnerAssetWriter extends InMemoryAssetWriter
3838
FakeWatcher.notifyWatchers(WatchEvent(
3939
ChangeType.REMOVE, p.absolute(id.package, p.fromUri(id.path))));
4040
}
41+
42+
@override
43+
Future<void> completeBuild() async {}
4144
}

_test_common/lib/runner_asset_writer_spy.dart

+3
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,7 @@ class RunnerAssetWriterSpy extends AssetWriterSpy implements RunnerAssetWriter {
2020
_assetsDeleted.add(id);
2121
return _delegate.delete(id);
2222
}
23+
24+
@override
25+
Future<void> completeBuild() async {}
2326
}

_test_common/lib/sdk.dart

+2-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import 'package:pub_semver/pub_semver.dart';
1212
import 'package:test/test.dart';
1313
import 'package:test_descriptor/test_descriptor.dart' as d;
1414

15-
String _dartBinary = p.join(sdkBin, 'dart');
16-
1715
final bool supportsUnsoundNullSafety =
1816
Version.parse(Platform.version.split(' ').first).major == 2;
1917

@@ -54,13 +52,13 @@ Future<Process> startPub(String package, String command,
5452
/// The [script] should be a relative path under [package].
5553
Future<ProcessResult> runDart(String package, String script,
5654
{Iterable<String>? args}) =>
57-
Process.run(_dartBinary, [script, ...?args],
55+
Process.run(dartBinary, [script, ...?args],
5856
workingDirectory: p.join(d.sandbox, package));
5957

6058
/// Starts the `dart` script [script] in [package] with [args].
6159
///
6260
/// The [script] should be a relative path under [package].
6361
Future<Process> startDart(String package, String script,
6462
{Iterable<String>? args}) =>
65-
Process.start(_dartBinary, [script, ...?args],
63+
Process.start(dartBinary, [script, ...?args],
6664
workingDirectory: p.join(d.sandbox, package));

_test_common/pubspec.yaml

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: _test_common
22
publish_to: none
33
description: Test infra for writing build tests. Is not published.
4-
resolution: workspace
4+
#resolution: workspace
55

66
environment:
77
sdk: ^3.5.0
@@ -19,3 +19,16 @@ dependencies:
1919
test: ^1.16.0
2020
test_descriptor: ^2.0.0
2121
watcher: ^1.0.0
22+
23+
dev_dependencies:
24+
dart_flutter_team_lints: ^3.1.0
25+
26+
dependency_overrides:
27+
build:
28+
path: ../build
29+
build_config:
30+
path: ../build_config
31+
build_runner_core:
32+
path: ../build_runner_core
33+
build_test:
34+
path: ../build_test

build_runner/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 2.4.14-wip
2+
3+
- Write generated assets at the end of a build to avoid invalidating other
4+
tools with a file watcher multiple times.
5+
16
## 2.4.13
27

38
- Bump the min sdk to 3.5.0.

build_runner/lib/src/watcher/delete_writer.dart

+5
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,9 @@ class OnDeleteWriter implements RunnerAssetWriter {
2929
Future writeAsString(AssetId id, String contents,
3030
{Encoding encoding = utf8}) =>
3131
_writer.writeAsString(id, contents, encoding: encoding);
32+
33+
@override
34+
Future<void> completeBuild() async {
35+
await _writer.completeBuild();
36+
}
3237
}

build_runner/pubspec.yaml

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
name: build_runner
2-
version: 2.4.13
2+
version: 2.4.14-wip
33
description: A build system for Dart code generation and modular compilation.
44
repository: https://github.com/dart-lang/build/tree/master/build_runner
5-
resolution: workspace
5+
#resolution: workspace
66

77
environment:
88
sdk: ^3.5.0
@@ -20,7 +20,7 @@ dependencies:
2020
build_config: ">=1.1.0 <1.2.0"
2121
build_daemon: ^4.0.0
2222
build_resolvers: ^2.0.0
23-
build_runner_core: ^7.2.0
23+
build_runner_core: ^8.0.0-wip
2424
code_builder: ^4.2.0
2525
collection: ^1.15.0
2626
crypto: ^3.0.0
@@ -53,10 +53,15 @@ dev_dependencies:
5353
path: ../_test_common
5454
build_test: ^2.0.0
5555
build_web_compilers: ^4.0.0
56+
dart_flutter_team_lints: ^3.1.0
5657
stream_channel: ^2.0.0
5758
test: ^1.25.5
5859
test_descriptor: ^2.0.0
5960
test_process: ^2.0.0
6061

62+
dependency_overrides:
63+
build_runner_core:
64+
path: ../build_runner_core
65+
6166
topics:
6267
- build-runner

build_runner/test/integration_tests/utils/build_descriptor.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'dart:isolate';
1010
import 'package:_test_common/sdk.dart';
1111
import 'package:async/async.dart';
1212
import 'package:build/build.dart';
13+
import 'package:build_runner_core/build_runner_core.dart';
1314
import 'package:package_config/package_config.dart';
1415
import 'package:path/path.dart' as p;
1516
import 'package:stack_trace/stack_trace.dart';
@@ -106,7 +107,7 @@ Future<BuildTool> package(Iterable<d.Descriptor> otherPackages,
106107
]).create();
107108
await Future.wait(otherPackages.map((d) => d.create()));
108109
await pubGet('a');
109-
return BuildTool._('dart', ['run', 'build_runner']);
110+
return BuildTool._(dartBinary, ['run', 'build_runner']);
110111
}
111112

112113
/// Create a package in [d.sandbox] with a `tool/build.dart` script using
@@ -145,7 +146,7 @@ Future<BuildTool> packageWithBuildScript(
145146
...contents
146147
]).create();
147148
await pubGet('a');
148-
return BuildTool._('dart', [p.join('tool', 'build.dart')]);
149+
return BuildTool._(dartBinary, [p.join('tool', 'build.dart')]);
149150
}
150151

151152
String _buildersFile(

build_runner_core/CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
## 7.3.3-wip
1+
## 8.0.0-wip
22

3+
- __Breaking__: Add `completeBuild` to `RunnerAssetWriter`, a method expected
4+
to be called by the build system at the end of a completed build.
5+
- Add `wrapInBatch` to obtain a reader/writer pair that will batch writes
6+
before flushing them at the end of a build.
37
- Bump the min sdk to 3.6.0-dev.228.
48
- Require analyzer ^6.9.0.
59
- Fix analyzer deprecations.

build_runner_core/lib/build_runner_core.dart

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
export 'package:build/build.dart' show PostProcessBuildStep, PostProcessBuilder;
66

7+
export 'src/asset/batch.dart' show wrapInBatch;
78
export 'src/asset/file_based.dart';
89
export 'src/asset/finalized_reader.dart';
910
export 'src/asset/reader.dart' show RunnerAssetReader;
+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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:async';
6+
import 'dart:convert';
7+
8+
import 'package:build/build.dart';
9+
import 'package:glob/glob.dart';
10+
import 'package:meta/meta.dart';
11+
12+
import '../environment/io_environment.dart';
13+
import 'reader.dart';
14+
import 'writer.dart';
15+
16+
/// A batch of file system writes that should be committed at once instead of
17+
/// when [AssetWriter.writeAsBytes] or [AssetWriter.writeAsString] is called.
18+
///
19+
/// During a typical build run emitting generated files one-by-one, it's
20+
/// possible that other running tools such as an analysis server will have to
21+
/// re-analyze incomplete states multiple times.
22+
/// By storing pending outputs in memory first and then committing them at the
23+
/// end of the build, we have a better view over that needs to happen.
24+
///
25+
/// The default [IOEnvironment] uses readers and writes that are batch-aware
26+
/// outside of low-memory mode.
27+
final class _FileSystemWriteBatch {
28+
final Map<AssetId, _PendingFileState> _pendingWrites = {};
29+
30+
_FileSystemWriteBatch._();
31+
32+
Future<void> completeWrites(RunnerAssetWriter writer) async {
33+
await Future.wait(_pendingWrites.keys.map((id) async {
34+
final pending = _pendingWrites[id]!;
35+
36+
if (pending.content case final content?) {
37+
await writer.writeAsBytes(id, content);
38+
} else {
39+
await writer.delete(id);
40+
}
41+
}));
42+
43+
_pendingWrites.clear();
44+
}
45+
}
46+
47+
/// Wraps a pair of a [RunnerAssetReader] with path-prividing capabilities and
48+
/// a [RunnerAssetWriter] into a pair of readers and writers that will
49+
/// internally buffer writes and only flush them in
50+
/// [RunnerAssetWriter.completeBuild].
51+
///
52+
/// The returned reader will see pending writes by the returned writer before
53+
/// they are flushed to the file system.
54+
(RunnerAssetReader, RunnerAssetWriter) wrapInBatch({
55+
required RunnerAssetReader reader,
56+
required PathProvidingAssetReader pathProvidingReader,
57+
required RunnerAssetWriter writer,
58+
}) {
59+
final batch = _FileSystemWriteBatch._();
60+
61+
return (
62+
BatchReader(reader, pathProvidingReader, batch),
63+
BatchWriter(writer, batch),
64+
);
65+
}
66+
67+
final class _PendingFileState {
68+
final List<int>? content;
69+
70+
const _PendingFileState(this.content);
71+
72+
bool get isDeleted => content == null;
73+
}
74+
75+
@internal
76+
final class BatchReader extends AssetReader
77+
implements RunnerAssetReader, PathProvidingAssetReader {
78+
final RunnerAssetReader _inner;
79+
final PathProvidingAssetReader _innerPathProviding;
80+
final _FileSystemWriteBatch _batch;
81+
82+
BatchReader(this._inner, this._innerPathProviding, this._batch);
83+
84+
_PendingFileState? _stateFor(AssetId id) {
85+
return _batch._pendingWrites[id];
86+
}
87+
88+
@override
89+
Future<bool> canRead(AssetId id) async {
90+
if (_stateFor(id) case final state?) {
91+
return !state.isDeleted;
92+
} else {
93+
return await _inner.canRead(id);
94+
}
95+
}
96+
97+
@override
98+
Stream<AssetId> findAssets(Glob glob, {String? package}) {
99+
return _inner
100+
.findAssets(glob, package: package)
101+
.where((asset) => _stateFor(asset)?.isDeleted != true);
102+
}
103+
104+
@override
105+
String pathTo(AssetId id) {
106+
return _innerPathProviding.pathTo(id);
107+
}
108+
109+
@override
110+
Future<List<int>> readAsBytes(AssetId id) async {
111+
if (_stateFor(id) case final state?) {
112+
if (state.isDeleted) {
113+
throw AssetNotFoundException(id);
114+
} else {
115+
return state.content!;
116+
}
117+
} else {
118+
return await _inner.readAsBytes(id);
119+
}
120+
}
121+
122+
@override
123+
Future<String> readAsString(AssetId id, {Encoding encoding = utf8}) async {
124+
if (_stateFor(id) case final state?) {
125+
if (state.isDeleted) {
126+
throw AssetNotFoundException(id);
127+
} else {
128+
return encoding.decode(state.content!);
129+
}
130+
} else {
131+
return await _inner.readAsString(id, encoding: encoding);
132+
}
133+
}
134+
}
135+
136+
@internal
137+
final class BatchWriter extends RunnerAssetWriter {
138+
final RunnerAssetWriter _inner;
139+
final _FileSystemWriteBatch _batch;
140+
141+
BatchWriter(this._inner, this._batch);
142+
143+
@override
144+
Future delete(AssetId id) async {
145+
_batch._pendingWrites[id] = const _PendingFileState(null);
146+
}
147+
148+
@override
149+
Future<void> writeAsBytes(AssetId id, List<int> bytes) async {
150+
_batch._pendingWrites[id] = _PendingFileState(bytes);
151+
}
152+
153+
@override
154+
Future<void> writeAsString(AssetId id, String contents,
155+
{Encoding encoding = utf8}) async {
156+
_batch._pendingWrites[id] = _PendingFileState(encoding.encode(contents));
157+
}
158+
159+
@override
160+
Future<void> completeBuild() async {
161+
await _batch.completeWrites(_inner);
162+
}
163+
}

build_runner_core/lib/src/asset/build_cache.dart

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class BuildCacheWriter implements RunnerAssetWriter {
8484
@override
8585
Future delete(AssetId id) =>
8686
_delegate.delete(_cacheLocation(id, _assetGraph, _rootPackage));
87+
88+
@override
89+
Future<void> completeBuild() async {}
8790
}
8891

8992
AssetId _cacheLocation(AssetId id, AssetGraph assetGraph, String rootPackage) {

build_runner_core/lib/src/asset/file_based.dart

+3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ class FileBasedAssetWriter implements RunnerAssetWriter {
107107
}
108108
});
109109
}
110+
111+
@override
112+
Future<void> completeBuild() async {}
110113
}
111114

112115
/// Returns the path to [id] for a given [packageGraph].

build_runner_core/lib/src/asset/writer.dart

+6
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@ typedef OnDelete = void Function(AssetId id);
1111

1212
abstract class RunnerAssetWriter implements AssetWriter {
1313
Future delete(AssetId id);
14+
15+
/// Called after each completed build.
16+
///
17+
/// Some [RunnerAssetWriter] implementations may buffer completed writes
18+
/// internally and flush them in [completeBuild].
19+
Future<void> completeBuild();
1420
}

0 commit comments

Comments
 (0)