Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to delay file system writes #3418

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 12 additions & 8 deletions build_runner/lib/src/daemon/daemon_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart' hide OutputLocation;
import 'package:build_daemon/data/server_log.dart';
import 'package:build_runner/src/entrypoint/options.dart';
import 'package:build_runner/src/generate/environment.dart';
import 'package:build_runner/src/package_graph/build_config_overrides.dart';
import 'package:build_runner/src/watcher/asset_change.dart';
import 'package:build_runner/src/watcher/change_filter.dart';
Expand Down Expand Up @@ -199,13 +200,17 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
var expectedDeletes = <AssetId>{};
var outputStreamController = StreamController<ServerLog>();

var environment = OverrideableEnvironment(
IOEnvironment(packageGraph,
outputSymlinksOnly: daemonOptions.outputSymlinksOnly),
onLog: (record) {
outputStreamController.add(ServerLog.fromLogRecord(record));
});

var environment = createDefaultEnvironment(
packageGraph: packageGraph,
assumeTty: null,
outputSymlinksOnly: daemonOptions.outputSymlinksOnly,
reader: null,
writer: null,
delayAssetWrites: daemonOptions.delayAssetWrites,
onLog: (record) =>
outputStreamController.add(ServerLog.fromLogRecord(record)),
verbose: true,
);
var daemonEnvironment = OverrideableEnvironment(environment,
writer: OnDeleteWriter(environment.writer, expectedDeletes.add));

Expand All @@ -220,7 +225,6 @@ class BuildRunnerDaemonBuilder implements DaemonBuilder {
logSubscription,
packageGraph: packageGraph,
deleteFilesByDefault: daemonOptions.deleteFilesByDefault,
delayWrites: daemonOptions.delayAssetWrites,
overrideBuildConfig: overrideBuildConfig,
skipBuildScriptCheck: daemonOptions.skipBuildScriptCheck,
enableLowResourcesMode: daemonOptions.enableLowResourcesMode,
Expand Down
1 change: 1 addition & 0 deletions build_runner/lib/src/entrypoint/base_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ abstract class BuildRunnerCommand extends Command<int> {
delayWritesOption,
help: 'Delays all file system interactions until the end of a build, '
'potentially reducing load on tools like the analysis server.',
defaultsTo: null,
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved
);
}

Expand Down
15 changes: 9 additions & 6 deletions build_runner/lib/src/entrypoint/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ class SharedOptions {
required this.isReleaseBuild,
required this.logPerformanceDir,
required this.enableExperiments,
required this.delayAssetWrites,
});
bool? delayAssetWrites,
}) :
// Delayed asset writes should be enabled by default if we're not in the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How confident are we that this is the right choice for the default?

// low-resources mode.
delayAssetWrites = delayAssetWrites ?? !enableLowResourcesMode;

SharedOptions.fromParsedArgs(ArgResults argResults,
Iterable<String> positionalArgs, String rootPackage, Command command)
Expand All @@ -127,7 +130,7 @@ class SharedOptions {
isReleaseBuild: argResults[releaseOption] as bool,
logPerformanceDir: argResults[logPerformanceOption] as String?,
enableExperiments: argResults[enableExperimentOption] as List<String>,
delayAssetWrites: argResults[delayWritesOption] as bool,
delayAssetWrites: argResults[delayWritesOption] as bool?,
);
}

Expand Down Expand Up @@ -209,7 +212,7 @@ class DaemonOptions extends WatchOptions {
logPerformanceDir: argResults[logPerformanceOption] as String?,
usePollingWatcher: argResults[usePollingWatcherOption] as bool,
enableExperiments: argResults[enableExperimentOption] as List<String>,
delayAssetWrites: argResults[delayWritesOption] as bool,
delayAssetWrites: argResults[delayWritesOption] as bool?,
);
}
}
Expand Down Expand Up @@ -275,7 +278,7 @@ class WatchOptions extends SharedOptions {
logPerformanceDir: argResults[logPerformanceOption] as String?,
usePollingWatcher: argResults[usePollingWatcherOption] as bool,
enableExperiments: argResults[enableExperimentOption] as List<String>,
delayAssetWrites: argResults[delayWritesOption] as bool,
delayAssetWrites: argResults[delayWritesOption] as bool?,
);
}

Expand Down Expand Up @@ -392,7 +395,7 @@ class ServeOptions extends WatchOptions {
logPerformanceDir: argResults[logPerformanceOption] as String?,
usePollingWatcher: argResults[usePollingWatcherOption] as bool,
enableExperiments: argResults[enableExperimentOption] as List<String>,
delayAssetWrites: argResults[delayWritesOption] as bool,
delayAssetWrites: argResults[delayWritesOption] as bool?,
);
}
}
Expand Down
3 changes: 1 addition & 2 deletions build_runner/lib/src/generate/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Future<BuildResult> build(List<BuilderApplication> builders,
trackPerformance ??= false;
verbose ??= false;

final environment = createEnvironment(
final environment = createDefaultEnvironment(
packageGraph: packageGraph,
assumeTty: assumeTty,
outputSymlinksOnly: outputSymlinksOnly,
Expand All @@ -98,7 +98,6 @@ Future<BuildResult> build(List<BuilderApplication> builders,
var options = await BuildOptions.create(
logSubscription,
deleteFilesByDefault: deleteFilesByDefault,
delayWrites: delayAssetWrites == true,
packageGraph: packageGraph,
skipBuildScriptCheck: skipBuildScriptCheck,
overrideBuildConfig: await findBuildConfigOverrides(
Expand Down
2 changes: 1 addition & 1 deletion build_runner/lib/src/generate/environment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:logging/logging.dart';

import '../logging/std_io_logging.dart';

BuildEnvironment createEnvironment({
BuildEnvironment createDefaultEnvironment({
required PackageGraph packageGraph,
required bool? assumeTty,
required bool outputSymlinksOnly,
Expand Down
3 changes: 1 addition & 2 deletions build_runner/lib/src/generate/watch_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Future<ServeHandler> watch(
trackPerformance ??= false;
verbose ??= false;

final environment = createEnvironment(
final environment = createDefaultEnvironment(
packageGraph: packageGraph,
assumeTty: assumeTty,
outputSymlinksOnly: outputSymlinksOnly,
Expand All @@ -87,7 +87,6 @@ Future<ServeHandler> watch(
var options = await BuildOptions.create(
logSubscription,
deleteFilesByDefault: deleteFilesByDefault,
delayWrites: delayAssetWrites == true,
packageGraph: packageGraph,
overrideBuildConfig: overrideBuildConfig,
debounceDelay: debounceDelay,
Expand Down
11 changes: 0 additions & 11 deletions build_runner/test/entrypoint/run_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,6 @@ main() {
});
}

void testBasicBuildCommandWithDelayedWrites(String command) {
test('is supported by the $command command', () async {
var args = ['build_runner', command, 'web', '--delayed-writes'];
expect(await runSingleBuild(command, args), ExitCode.success.code);
expectOutput('web/main.dart.js', exists: true);
expectOutput('test/hello_test.dart.browser_test.dart.js',
exists: false);
});
}

void testBuildCommandWithOutput(String command) {
test('works with -o and the $command command', () async {
var outputDirName = 'foo';
Expand All @@ -136,7 +126,6 @@ main() {

for (var command in ['build', 'serve', 'watch']) {
testBasicBuildCommand(command);
testBasicBuildCommandWithDelayedWrites(command);
testBuildCommandWithOutput(command);
}

Expand Down
24 changes: 17 additions & 7 deletions build_runner_core/lib/src/asset/writer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:convert';

import 'package:build/build.dart';
import 'package:glob/glob.dart';
import 'package:meta/meta.dart';

import 'reader.dart';

Expand All @@ -19,7 +20,8 @@ abstract class RunnerAssetWriter implements AssetWriter {
/// Potentially runs pending work that needs to happen just before the build
/// completes, after all other interactions with this [AssetWriter] have
/// already been completed.
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved
FutureOr<void> onBuildComplete() {}
@mustCallSuper
FutureOr<void> onBuildComplete();
}

/// A [RunnerAssetWriter] implementation that performs all writes at once in
Expand All @@ -30,8 +32,12 @@ abstract class RunnerAssetWriter implements AssetWriter {
class DelayedAssetWriter implements RunnerAssetWriter {
final RunnerAssetWriter _delegate;

/// A map of asset ids to content that has been written with the [AssetWriter]
/// API but not yet been flushed to the underlying delegate (in [onBuildComplete]).
///
/// The values are non-null if the contents of the file have been overwritten,
/// or null if the asset has been deleted.
final Map<AssetId, List<int>?> overlay = {};
jakemac53 marked this conversation as resolved.
Show resolved Hide resolved
final List<Future Function()> _work = [];

DelayedAssetWriter(this._delegate);

Expand All @@ -44,22 +50,26 @@ class DelayedAssetWriter implements RunnerAssetWriter {
@override
Future delete(AssetId id) async {
overlay[id] = null;
_work.add(() => _delegate.delete(id));
}

@override
Future<void> onBuildComplete() async {
final todos = _work.toList();
_work.clear();
await Future.wait(overlay.entries.map((entry) {
final value = entry.value;
if (value == null) {
return _delegate.delete(entry.key);
} else {
return _delegate.writeAsBytes(entry.key, value);
}
}));

await Future.wait([for (final unit in todos) unit()]);
await _delegate.onBuildComplete();
overlay.clear();
}

@override
Future<void> writeAsBytes(AssetId id, List<int> bytes) async {
overlay[id] = bytes;
_work.add(() => _delegate.writeAsBytes(id, bytes));
}

@override
Expand Down
11 changes: 2 additions & 9 deletions build_runner_core/lib/src/generate/build_definition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -543,15 +543,8 @@ class _Loader {
var done = false;
while (!done) {
try {
int choice;

if (_options.delayWrites) {
choice = await _environment.prompt('Overwrite these files?',
['Overwrite', 'Cancel build', 'List conflicts']);
} else {
choice = await _environment.prompt('Delete these files?',
['Delete', 'Cancel build', 'List conflicts']);
}
var choice = await _environment.prompt('Delete these files?',
['Delete', 'Cancel build', 'List conflicts']);

switch (choice) {
case 0:
Expand Down
8 changes: 7 additions & 1 deletion build_runner_core/lib/src/generate/build_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,13 @@ class _SingleBuild {
try {
await _environment.writer.onBuildComplete();
} catch (e, s) {
_logger.severe('Assets could not be written', e, s);
_logger.severe(
'Could not complete writes for this build.\n'
'The current state of the file system may not reflect the expected '
'build outputs.',
e,
s,
);
result = BuildResult(BuildStatus.failure, result.outputs,
performance: result.performance);
}
Expand Down
4 changes: 0 additions & 4 deletions build_runner_core/lib/src/generate/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ class BuildFilter {
/// Manages setting up consistent defaults for all options and build modes.
class BuildOptions {
final bool deleteFilesByDefault;
final bool delayWrites;
final bool enableLowResourcesMode;
final StreamSubscription logListener;

Expand All @@ -155,7 +154,6 @@ class BuildOptions {
BuildOptions._({
required this.debounceDelay,
required this.deleteFilesByDefault,
required this.delayWrites,
required this.enableLowResourcesMode,
required this.logListener,
required this.packageGraph,
Expand All @@ -174,7 +172,6 @@ class BuildOptions {
LogSubscription logSubscription, {
Duration debounceDelay = const Duration(milliseconds: 250),
bool deleteFilesByDefault = false,
bool delayWrites = false,
bool enableLowResourcesMode = false,
required PackageGraph packageGraph,
Map<String, BuildConfig> overrideBuildConfig = const {},
Expand Down Expand Up @@ -216,7 +213,6 @@ feature, you may need to run `dart run build_runner clean` and then rebuild.
return BuildOptions._(
debounceDelay: debounceDelay,
deleteFilesByDefault: deleteFilesByDefault,
delayWrites: delayWrites,
enableLowResourcesMode: enableLowResourcesMode,
logListener: logSubscription.logListener,
packageGraph: packageGraph,
Expand Down
39 changes: 38 additions & 1 deletion build_runner_core/test/asset/writer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import 'package:_test_common/common.dart';
import 'package:build/build.dart';
import 'package:build_runner_core/build_runner_core.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:watcher/watcher.dart';

void main() {
late InMemoryRunnerAssetReader underlyingReader;
Expand Down Expand Up @@ -36,7 +38,7 @@ void main() {
expect(underlyingWriter.assets, {a: decodedMatches('a')});
});

test('does not write twice', () async {
test('does not write data that has already been written', () async {
final writer = DelayedAssetWriter(underlyingWriter);
final asset = makeAssetId('a|lib/a.dart');

Expand All @@ -49,6 +51,25 @@ void main() {
expect(underlyingWriter.assets, isEmpty);
});

test('avoids duplicate writes on the same asset', () async {
final writer = DelayedAssetWriter(underlyingWriter);
final asset = makeAssetId('a|lib/a.dart');

expect(
FakeWatcher(p.absolute('a')).events,
emitsInOrder([
isA<WatchEvent>().having(
(e) => e.path, 'path', p.absolute('a', 'lib', 'a.dart')),
isA<WatchEvent>().having(
(e) => e.path, 'path', p.absolute('a', 'lib', 'a.dart.copy')),
]));

await writer.writeAsString(asset, 'old content');
await writer.writeAsString(asset, 'new content');
await writer.writeAsString(asset.addExtension('.copy'), 'new content');
await writer.onBuildComplete();
});

group('has consistent reader', () {
test('reading unmodified sources', () async {
final asset = makeAssetId('a|lib/a.dart');
Expand Down Expand Up @@ -89,6 +110,22 @@ void main() {
expect(await reader.readAsBytes(asset), decodedMatches('contents'));
});

test('does not use overlay after flush', () async {
final asset = makeAssetId('a|lib/a.dart');
underlyingWriter.assets[asset] = [1, 2, 3];

final writer = DelayedAssetWriter(underlyingWriter);
final reader = writer.reader(underlyingReader, rootPackage);

await writer.writeAsString(asset, 'contents');
await writer.onBuildComplete();
// "contents" should have been written, but there may be another tool
// overwriting the file afterwards.
underlyingWriter.assets[asset] = [4, 5, 6];

expect(await reader.readAsBytes(asset), [4, 5, 6]);
});

test('finding right assets', () async {
final a = makeAssetId('a|lib/a.dart');
final b = makeAssetId('a|lib/b.dart');
Expand Down