Repository: dart-lang/build
Pull request #3901: Stop using Future.wait in `build_impl`.
Author: login: davidmorgan
Labels: name: skip-changelog-check
Created: 2025-03-04T08:24:45Z | Merged: 2025-03-04T12:45:23Z

It only increases performance in some situations with an I/O bottleneck, which is not obviously the case; for now let's remove as it makes it clearer what's going on. Just do tasks one by one.

It's still the case that there is some "parallelism" because a build action can require an input from an action in an earlier phase that didn't run yet. So the max running actions is one per phase.

Speeds things up a bit.

```
before this PR:

shape,libraries,clean/ms,no changes/ms,incremental/ms
loop,1,24385,4365,5483
loop,100,26646,4417,7557
loop,250,30159,4582,12666
loop,500,41991,5000,25656
loop,750,59208,6361,43598
loop,1000,78480,8256,69272

after:
shape,libraries,clean/ms,no changes/ms,incremental/ms
loop,1,22517,4053,5170
loop,100,25499,4039,7974
loop,250,29008,4456,12994
loop,500,39135,5091,24792
loop,750,54465,6100,38380
loop,1000,72807,7287,59647
```


Diff:
diff --git a/build_runner_core/lib/src/generate/build_impl.dart b/build_runner_core/lib/src/generate/build_impl.dart
index ef3bd9ba47..cfc1f7a130 100644
--- a/build_runner_core/lib/src/generate/build_impl.dart
+++ b/build_runner_core/lib/src/generate/build_impl.dart
@@ -13,7 +13,6 @@ import 'package:build/src/internal.dart';
 import 'package:crypto/crypto.dart';
 import 'package:logging/logging.dart';
 import 'package:path/path.dart' as p;
-import 'package:pool/pool.dart';
 import 'package:watcher/watcher.dart';
 
 import '../asset/finalized_reader.dart';
@@ -154,7 +153,6 @@ class _SingleBuild {
   final AssetGraph _assetGraph;
   final Set<BuildFilter> _buildFilters;
   final List<BuildPhase> _buildPhases;
-  final List<Pool> _buildPhasePool;
   final BuildEnvironment _environment;
   final _lazyPhases = <String, Future<Iterable<AssetId>>>{};
   final _lazyGlobs = <AssetId, Future<void>>{};
@@ -183,11 +181,6 @@ class _SingleBuild {
   ) : _assetGraph = buildImpl.assetGraph,
       _buildFilters = buildFilters,
       _buildPhases = buildImpl._buildPhases,
-      _buildPhasePool = List.generate(
-        buildImpl._buildPhases.length,
-        (_) => Pool(buildPhasePoolSize),
-        growable: false,
-      ),
       _environment = buildImpl._environment,
       _packageGraph = buildImpl._packageGraph,
       _targetGraph = buildImpl._targetGraph,
@@ -398,34 +391,34 @@ class _SingleBuild {
     var phase = _buildPhases[phaseNumber];
     var packageNode = _packageGraph[package]!;
 
-    await Future.wait(
-      _assetGraph.outputsForPhase(package, phaseNumber).map((node) async {
-        if (!shouldBuildForDirs(
-          node.id,
-          buildDirs: _buildPaths(_buildDirs),
-          buildFilters: _buildFilters,
-          phase: phase,
-          targetGraph: _targetGraph,
-        )) {
-          return;
-        }
+    for (final node in _assetGraph
+        .outputsForPhase(package, phaseNumber)
+        .toList(growable: false)) {
+      if (!shouldBuildForDirs(
+        node.id,
+        buildDirs: _buildPaths(_buildDirs),
+        buildFilters: _buildFilters,
+        phase: phase,
+        targetGraph: _targetGraph,
+      )) {
+        continue;
+      }
 
-        // Don't build for inputs that aren't visible. This can happen for
-        // placeholder nodes like `test/$test$` that are added to each package,
-        // since the test dir is not part of the build for non-root packages.
-        if (!_targetGraph.isVisibleInBuild(node.id, packageNode)) return;
-
-        var input = _assetGraph.get(node.primaryInput)!;
-        if (input is GeneratedAssetNode) {
-          if (input.state != NodeState.upToDate) {
-            await _runLazyPhaseForInput(input.phaseNumber, input.primaryInput);
-          }
-          if (!input.wasOutput) return;
-          if (input.isFailure) return;
+      // Don't build for inputs that aren't visible. This can happen for
+      // placeholder nodes like `test/$test$` that are added to each package,
+      // since the test dir is not part of the build for non-root packages.
+      if (!_targetGraph.isVisibleInBuild(node.id, packageNode)) continue;
+
+      var input = _assetGraph.get(node.primaryInput)!;
+      if (input is GeneratedAssetNode) {
+        if (input.state != NodeState.upToDate) {
+          await _runLazyPhaseForInput(input.phaseNumber, input.primaryInput);
         }
-        ids.add(input.id);
-      }),
-    );
+        if (!input.wasOutput) continue;
+        if (input.isFailure) continue;
+      }
+      ids.add(input.id);
+    }
     return ids;
   }
 
@@ -439,13 +432,11 @@ class _SingleBuild {
     InBuildPhase action,
     Iterable<AssetId> primaryInputs,
   ) async {
-    var outputLists = await Future.wait(
-      primaryInputs.map((input) => _runForInput(phaseNumber, action, input)),
-    );
-    return outputLists.fold<List<AssetId>>(
-      <AssetId>[],
-      (combined, next) => combined..addAll(next),
-    );
+    final outputs = <AssetId>[];
+    for (final input in primaryInputs) {
+      outputs.addAll(await _runForInput(phaseNumber, action, input));
+    }
+    return outputs;
   }
 
   /// Lazily runs [phaseNumber] with [input]..
@@ -485,113 +476,111 @@ class _SingleBuild {
     int phaseNumber,
     InBuildPhase phase,
     AssetId input,
-  ) {
-    return _buildPhasePool[phaseNumber].withResource(() {
-      final builder = phase.builder;
-      var tracker = _performanceTracker.addBuilderAction(
-        input,
-        phase.builderLabel,
+  ) async {
+    final builder = phase.builder;
+    var tracker = _performanceTracker.addBuilderAction(
+      input,
+      phase.builderLabel,
+    );
+    return tracker.track(() async {
+      var builderOutputs = expectedOutputs(builder, input);
+
+      // Add `builderOutputs` to the primary outputs of the input.
+      var inputNode = _assetGraph.get(input)!;
+      assert(
+        inputNode.primaryOutputs.containsAll(builderOutputs),
+        // ignore: prefer_interpolation_to_compose_strings
+        'input $input with builder $builder missing primary outputs: \n'
+                'Got ${inputNode.primaryOutputs.join(', ')} '
+                'which was missing:\n' +
+            builderOutputs
+                .where((id) => !inputNode.primaryOutputs.contains(id))
+                .join(', '),
       );
-      return tracker.track(() async {
-        var builderOutputs = expectedOutputs(builder, input);
-
-        // Add `builderOutputs` to the primary outputs of the input.
-        var inputNode = _assetGraph.get(input)!;
-        assert(
-          inputNode.primaryOutputs.containsAll(builderOutputs),
-          // ignore: prefer_interpolation_to_compose_strings
-          'input $input with builder $builder missing primary outputs: \n'
-                  'Got ${inputNode.primaryOutputs.join(', ')} '
-                  'which was missing:\n' +
-              builderOutputs
-                  .where((id) => !inputNode.primaryOutputs.contains(id))
-                  .join(', '),
-        );
 
-        var readerWriter = SingleStepReaderWriter(
-          runningBuild: RunningBuild(
-            packageGraph: _packageGraph,
-            targetGraph: _targetGraph,
-            assetGraph: _assetGraph,
-            nodeBuilder: _buildAsset,
-            globNodeBuilder: _buildGlobNode,
-          ),
-          runningBuildStep: RunningBuildStep(
-            phaseNumber: phaseNumber,
+      var readerWriter = SingleStepReaderWriter(
+        runningBuild: RunningBuild(
+          packageGraph: _packageGraph,
+          targetGraph: _targetGraph,
+          assetGraph: _assetGraph,
+          nodeBuilder: _buildAsset,
+          globNodeBuilder: _buildGlobNode,
+        ),
+        runningBuildStep: RunningBuildStep(
+          phaseNumber: phaseNumber,
 
-            buildPhase: phase,
-            primaryPackage: input.package,
-          ),
-          readerWriter: _readerWriter,
-          inputTracker: InputTracker(_readerWriter.filesystem),
-          assetsWritten: {},
-        );
+          buildPhase: phase,
+          primaryPackage: input.package,
+        ),
+        readerWriter: _readerWriter,
+        inputTracker: InputTracker(_readerWriter.filesystem),
+        assetsWritten: {},
+      );
 
-        if (!await tracker.trackStage(
-          'Setup',
-          () => _buildShouldRun(builderOutputs, readerWriter),
-        )) {
-          return <AssetId>[];
-        }
+      if (!await tracker.trackStage(
+        'Setup',
+        () => _buildShouldRun(builderOutputs, readerWriter),
+      )) {
+        return <AssetId>[];
+      }
 
-        await _cleanUpStaleOutputs(builderOutputs);
-        await FailureReporter.clean(phaseNumber, input);
+      await _cleanUpStaleOutputs(builderOutputs);
+      await FailureReporter.clean(phaseNumber, input);
 
-        // Clear input tracking accumulated during `_buildShouldRun`.
-        readerWriter.inputTracker.clear();
+      // Clear input tracking accumulated during `_buildShouldRun`.
+      readerWriter.inputTracker.clear();
 
-        var actionDescription = _actionLoggerName(
-          phase,
+      var actionDescription = _actionLoggerName(
+        phase,
+        input,
+        _packageGraph.root.name,
+      );
+      var logger = BuildForInputLogger(Logger(actionDescription));
+
+      actionsStartedCount++;
+      pendingActions
+          .putIfAbsent(phaseNumber, () => <String>{})
+          .add(actionDescription);
+
+      var unusedAssets = <AssetId>{};
+      await tracker.trackStage(
+        'Build',
+        () => runBuilder(
+          builder,
+          [input],
+          readerWriter,
+          readerWriter,
+          PerformanceTrackingResolvers(_resolvers, tracker),
+          logger: logger,
+          resourceManager: _resourceManager,
+          stageTracker: tracker,
+          reportUnusedAssetsForInput:
+              (_, assets) => unusedAssets.addAll(assets),
+          packageConfig: _packageGraph.asPackageConfig,
+        ).catchError((void _) {
+          // Errors tracked through the logger
+        }),
+      );
+      actionsCompletedCount++;
+      hungActionsHeartbeat.ping();
+      pendingActions[phaseNumber]!.remove(actionDescription);
+
+      // Reset the state for all the `builderOutputs` nodes based on what was
+      // read and written.
+      await tracker.trackStage(
+        'Finalize',
+        () => _setOutputsState(
           input,
-          _packageGraph.root.name,
-        );
-        var logger = BuildForInputLogger(Logger(actionDescription));
-
-        actionsStartedCount++;
-        pendingActions
-            .putIfAbsent(phaseNumber, () => <String>{})
-            .add(actionDescription);
-
-        var unusedAssets = <AssetId>{};
-        await tracker.trackStage(
-          'Build',
-          () => runBuilder(
-            builder,
-            [input],
-            readerWriter,
-            readerWriter,
-            PerformanceTrackingResolvers(_resolvers, tracker),
-            logger: logger,
-            resourceManager: _resourceManager,
-            stageTracker: tracker,
-            reportUnusedAssetsForInput:
-                (_, assets) => unusedAssets.addAll(assets),
-            packageConfig: _packageGraph.asPackageConfig,
-          ).catchError((void _) {
-            // Errors tracked through the logger
-          }),
-        );
-        actionsCompletedCount++;
-        hungActionsHeartbeat.ping();
-        pendingActions[phaseNumber]!.remove(actionDescription);
-
-        // Reset the state for all the `builderOutputs` nodes based on what was
-        // read and written.
-        await tracker.trackStage(
-          'Finalize',
-          () => _setOutputsState(
-            input,
-            builderOutputs,
-            readerWriter,
-            readerWriter.inputTracker,
-            actionDescription,
-            logger.errorsSeen,
-            unusedAssets: unusedAssets,
-          ),
-        );
+          builderOutputs,
+          readerWriter,
+          readerWriter.inputTracker,
+          actionDescription,
+          logger.errorsSeen,
+          unusedAssets: unusedAssets,
+        ),
+      );
 
-        return readerWriter.assetsWritten;
-      });
+      return readerWriter.assetsWritten;
     });
   }
 
@@ -616,34 +605,29 @@ class _SingleBuild {
     int actionNum,
     PostBuildAction action,
   ) async {
-    var anchorNodes =
-        _assetGraph.packageNodes(action.package).where((node) {
-          if (node is PostProcessAnchorNode && node.actionNumber == actionNum) {
-            var inputNode = _assetGraph.get(node.primaryInput);
-            if (inputNode is SourceAssetNode) {
-              return true;
-            } else if (inputNode is GeneratedAssetNode) {
-              return inputNode.wasOutput &&
-                  !inputNode.isFailure &&
-                  inputNode.state == NodeState.upToDate;
-            }
-          }
-          return false;
-        }).cast<PostProcessAnchorNode>();
-    var outputLists = await Future.wait(
-      anchorNodes.map(
-        (anchorNode) => _runPostProcessBuilderForAnchor(
-          phaseNum,
-          actionNum,
-          action.builder,
-          anchorNode,
-        ),
-      ),
-    );
-    return outputLists.fold<List<AssetId>>(
-      <AssetId>[],
-      (combined, next) => combined..addAll(next),
-    );
+    final outputs = <AssetId>[];
+    for (final node in _assetGraph
+        .packageNodes(action.package)
+        .toList(growable: false)) {
+      if (node is! PostProcessAnchorNode) continue;
+      if (node.actionNumber != actionNum) continue;
+      final inputNode = _assetGraph.get(node.primaryInput);
+      if (inputNode is SourceAssetNode ||
+          inputNode is GeneratedAssetNode &&
+              inputNode.wasOutput &&
+              !inputNode.isFailure &&
+              inputNode.state == NodeState.upToDate) {
+        outputs.addAll(
+          await _runPostProcessBuilderForAnchor(
+            phaseNum,
+            actionNum,
+            action.builder,
+            node,
+          ),
+        );
+      }
+    }
+    return outputs;
   }
 
   Future<Iterable<AssetId>> _runPostProcessBuilderForAnchor(
@@ -836,13 +820,14 @@ class _SingleBuild {
   /// This should be called after deciding that an asset really needs to be
   /// regenerated based on its inputs hash changing. All assets in [outputs]
   /// must correspond to a [GeneratedAssetNode].
-  Future<void> _cleanUpStaleOutputs(Iterable<AssetId> outputs) => Future.wait(
-    outputs
-        .map(_assetGraph.get)
-        .cast<GeneratedAssetNode>()
-        .where((n) => n.wasOutput)
-        .map((n) => _delete(n.id)),
-  );
+  Future<void> _cleanUpStaleOutputs(Iterable<AssetId> outputs) async {
+    for (final output in outputs) {
+      final node = _assetGraph.get(output)!;
+      if (node is GeneratedAssetNode && node.wasOutput) {
+        await _delete(output);
+      }
+    }
+  }
 
   Future<void> _buildGlobNode(GlobAssetNode globNode) async {
     if (globNode.state == NodeState.upToDate) return;
@@ -860,9 +845,11 @@ class _SingleBuild {
               .where((n) => globNode.glob.matches(n.id.path))
               .toList();
 
-      await Future.wait(
-        potentialNodes.whereType<GeneratedAssetNode>().map(_buildAsset),
-      );
+      for (final node in potentialNodes) {
+        if (node is GeneratedAssetNode) {
+          await _buildAsset(node);
+        }
+      }
 
       var actualMatches = <AssetId>[];
       for (var node in potentialNodes) {
@@ -902,29 +889,25 @@ class _SingleBuild {
     var builderOptionsNode = _assetGraph.get(builderOptionsId)!;
     combine(builderOptionsNode.lastKnownDigest!.bytes as Uint8List);
 
-    // Limit the total number of digests we are computing at a time. Otherwise
-    // this can overload the event queue.
-    await Future.wait(
-      ids.map((id) async {
-        var node = _assetGraph.get(id)!;
-        if (node is GlobAssetNode) {
-          await _buildGlobNode(node);
-        } else if (!await reader.canRead(id)) {
-          // We want to add something here, a missing/unreadable input should be
-          // different from no input at all.
-          //
-          // This needs to be unique per input so we use the md5 hash of the id.
-          combine(md5.convert(id.toString().codeUnits).bytes as Uint8List);
-          return;
-        } else {
-          if (node.lastKnownDigest == null) {
-            await reader.cache.invalidate([id]);
-            node.lastKnownDigest = await reader.digest(id);
-          }
+    for (final id in ids) {
+      var node = _assetGraph.get(id)!;
+      if (node is GlobAssetNode) {
+        await _buildGlobNode(node);
+      } else if (!await reader.canRead(id)) {
+        // We want to add something here, a missing/unreadable input should be
+        // different from no input at all.
+        //
+        // This needs to be unique per input so we use the md5 hash of the id.
+        combine(md5.convert(id.toString().codeUnits).bytes as Uint8List);
+        continue;
+      } else {
+        if (node.lastKnownDigest == null) {
+          await reader.cache.invalidate([id]);
+          node.lastKnownDigest = await reader.digest(id);
         }
-        combine(node.lastKnownDigest!.bytes as Uint8List);
-      }),
-    );
+      }
+      combine(node.lastKnownDigest!.bytes as Uint8List);
+    }
 
     return Digest(combinedBytes);
   }
diff --git a/build_runner_core/lib/src/util/constants.dart b/build_runner_core/lib/src/util/constants.dart
index fc18414fd6..3c3b25dc37 100644
--- a/build_runner_core/lib/src/util/constants.dart
+++ b/build_runner_core/lib/src/util/constants.dart
@@ -100,6 +100,3 @@ final sdkBin = p.join(sdkPath, 'bin');
 
 /// The path to the sdk on the current platform.
 final sdkPath = p.dirname(p.dirname(Platform.resolvedExecutable));
-
-/// The maximum number of concurrent actions to run per build phase.
-const buildPhasePoolSize = 16;
diff --git a/build_runner_core/test/generate/build_test.dart b/build_runner_core/test/generate/build_test.dart
index be14732d3b..6b6da74741 100644
--- a/build_runner_core/test/generate/build_test.dart
+++ b/build_runner_core/test/generate/build_test.dart
@@ -16,7 +16,6 @@ import 'package:build_runner_core/src/asset_graph/graph.dart';
 import 'package:build_runner_core/src/asset_graph/node.dart';
 import 'package:build_runner_core/src/generate/options.dart'
     show defaultNonRootVisibleAssets;
-import 'package:build_runner_core/src/util/constants.dart';
 import 'package:glob/glob.dart';
 import 'package:test/test.dart';
 
@@ -157,9 +156,9 @@ void main() {
       );
     });
 
-    test('runs a max of 16 concurrent actions per phase', () async {
+    test('runs a max of one concurrent action per phase', () async {
       var assets = <String, String>{};
-      for (var i = 0; i < buildPhasePoolSize * 2; i++) {
+      for (var i = 0; i < 2; i++) {
         assets['a|web/$i.txt'] = '$i';
       }
       var concurrentCount = 0;
@@ -178,8 +177,7 @@ void main() {
                       concurrentCount,
                       maxConcurrentCount,
                     );
-                    if (concurrentCount >= buildPhasePoolSize &&
-                        !reachedMax.isCompleted) {
+                    if (concurrentCount >= 1 && !reachedMax.isCompleted) {
                       await Future<void>.delayed(
                         const Duration(milliseconds: 100),
                       );
@@ -199,7 +197,7 @@ void main() {
         assets,
         outputs: {},
       );
-      expect(maxConcurrentCount, buildPhasePoolSize);
+      expect(maxConcurrentCount, 1);
     });
 
     group('with root package inputs', () {


Reviews:
user: login: jensjoha
state: APPROVED
author_association: NONE
submitted_at: 2025-03-04T12:21:16Z
user: login: davidmorgan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T12:44:47Z

Review comments:
diff_hunk: @@ -616,34 +613,27 @@ class _SingleBuild {
     int actionNum,
     PostBuildAction action,
   ) async {
-    var anchorNodes =
-        _assetGraph.packageNodes(action.package).where((node) {
-          if (node is PostProcessAnchorNode && node.actionNumber == actionNum) {
-            var inputNode = _assetGraph.get(node.primaryInput);
-            if (inputNode is SourceAssetNode) {
-              return true;
-            } else if (inputNode is GeneratedAssetNode) {
-              return inputNode.wasOutput &&
-                  !inputNode.isFailure &&
-                  inputNode.state == NodeState.upToDate;
-            }
-          }
-          return false;
-        }).cast<PostProcessAnchorNode>();
-    var outputLists = await Future.wait(
-      anchorNodes.map(
-        (anchorNode) => _runPostProcessBuilderForAnchor(
-          phaseNum,
-          actionNum,
-          action.builder,
-          anchorNode,
-        ),
-      ),
-    );
-    return outputLists.fold<List<AssetId>>(
-      <AssetId>[],
-      (combined, next) => combined..addAll(next),
-    );
+    final outputs = <AssetId>[];
+    for (final node in _assetGraph.packageNodes(action.package).toList()) {
path: build_runner_core/lib/src/generate/build_impl.dart
user: login: jensjoha
body: if the `.toList()` is needed you should probably pass `growable: false`.
created_at: 2025-03-04T12:16:30Z
author_association: NONE
diff_hunk: @@ -185,7 +185,7 @@ class _SingleBuild {
       _buildPhases = buildImpl._buildPhases,
       _buildPhasePool = List.generate(
         buildImpl._buildPhases.length,
-        (_) => Pool(buildPhasePoolSize),
+        (_) => Pool(1),
path: build_runner_core/lib/src/generate/build_impl.dart
user: login: jensjoha
body: does a pool of 1 make sense?
created_at: 2025-03-04T12:21:09Z
author_association: NONE
diff_hunk: @@ -616,34 +613,27 @@ class _SingleBuild {
     int actionNum,
     PostBuildAction action,
   ) async {
-    var anchorNodes =
-        _assetGraph.packageNodes(action.package).where((node) {
-          if (node is PostProcessAnchorNode && node.actionNumber == actionNum) {
-            var inputNode = _assetGraph.get(node.primaryInput);
-            if (inputNode is SourceAssetNode) {
-              return true;
-            } else if (inputNode is GeneratedAssetNode) {
-              return inputNode.wasOutput &&
-                  !inputNode.isFailure &&
-                  inputNode.state == NodeState.upToDate;
-            }
-          }
-          return false;
-        }).cast<PostProcessAnchorNode>();
-    var outputLists = await Future.wait(
-      anchorNodes.map(
-        (anchorNode) => _runPostProcessBuilderForAnchor(
-          phaseNum,
-          actionNum,
-          action.builder,
-          anchorNode,
-        ),
-      ),
-    );
-    return outputLists.fold<List<AssetId>>(
-      <AssetId>[],
-      (combined, next) => combined..addAll(next),
-    );
+    final outputs = <AssetId>[];
+    for (final node in _assetGraph.packageNodes(action.package).toList()) {
path: build_runner_core/lib/src/generate/build_impl.dart
user: login: davidmorgan
body: Done.
created_at: 2025-03-04T12:24:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -185,7 +185,7 @@ class _SingleBuild {
       _buildPhases = buildImpl._buildPhases,
       _buildPhasePool = List.generate(
         buildImpl._buildPhases.length,
-        (_) => Pool(buildPhasePoolSize),
+        (_) => Pool(1),
path: build_runner_core/lib/src/generate/build_impl.dart
user: login: davidmorgan
body: Good question! Looking at where the pool is used, there are no concurrent calls possible any more. So, removed. Thanks.
created_at: 2025-03-04T12:34:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-04T08:25:38Z
body: ## Package publishing

| Package | Version | Status | Publish tag (post-merge) |
| :--- | ---: | :--- | ---: |
| package:build | 2.4.3-wip | WIP (no publish necessary) |  |
| package:build_config | 1.1.3-wip | WIP (no publish necessary) |  |
| package:build_daemon | 4.0.5-wip | WIP (no publish necessary) |  |
| package:build_modules | 5.0.12-wip | WIP (no publish necessary) |  |
| package:build_resolvers | 2.4.5-wip | WIP (no publish necessary) |  |
| package:build_runner | 2.4.16-wip | WIP (no publish necessary) |  |
| package:build_runner_core | 8.0.1-wip | WIP (no publish necessary) |  |
| package:build_test | 3.0.0-wip | WIP (no publish necessary) |  |
| package:build_web_compilers | 4.1.2 | **ready to publish** | [`build_web_compilers-v4.1.2`](https://github.com/dart-lang/build/releases/new?tag=build_web_compilers-v4.1.2&title=package%3Abuild_web_compilers+v4.1.2&body=-+Bump+the+min+SDK+to+3.7.0.%0A-+Use+%60build_test%60+3.0.0.%0A-+Use+support-detection+scripts+emitted+by+%60dart2wasm%60.) |
| package:scratch_space | 1.0.3-wip | WIP (no publish necessary) |  |

Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-04T08:26:41Z
body: ## PR Health 



author_association: NONE
user: login: gmpassos
created_at: 2025-03-04T08:40:24Z
body: NOTE: CI servers are usually slower in terms of disk performance.
author_association: NONE
user: login: davidmorgan
created_at: 2025-03-04T08:56:45Z
body: > NOTE: CI servers are usually slower in terms of disk performance.

That should still be fine--`build_runner` spends almost no time on I/O. If it does turn out to slow things down, hopefully we notice, as that gives something to fix :) thanks!
author_association: CONTRIBUTOR