Skip to content

Commit 78fcc41

Browse files
authored
Add what changed and what is built to verbose logging. (#3937)
* Add what changed and what is built to verbose logging. * Address review comments.
1 parent bf5df1a commit 78fcc41

File tree

7 files changed

+282
-13
lines changed

7 files changed

+282
-13
lines changed

build_runner/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Remove `OnDeleteWriter`, add functionality to `ReaderWriter`.
1212
- Add `NodeType` to `AssetNode`, remove subtypes. Make mutations explicit.
1313
- Use `built_value` for `AssetNode` and related types.
14+
- Add details of what changed and what is built to `--verbose` logging.
1415

1516
## 2.4.15
1617

build_runner/lib/src/logging/std_io_logging.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ StringBuffer colorLog(LogRecord record, {required bool verbose}) {
2626
final level = color.wrap('[${record.level}]');
2727
final eraseLine = ansiOutputEnabled && !verbose ? '\x1b[2K\r' : '';
2828
var lines = <Object>[
29-
'$eraseLine$level ${_recordHeader(record, verbose)}${record.message}',
29+
'$eraseLine$level ${_recordHeader(record)}${record.message}',
3030
];
3131

3232
if (record.error != null) {
@@ -64,9 +64,9 @@ void _stdIOLogListener(LogRecord record, {required bool verbose}) =>
6464

6565
/// Filter out the Logger names which aren't coming from specific builders and
6666
/// splits the header for levels >= WARNING.
67-
String _recordHeader(LogRecord record, bool verbose) {
67+
String _recordHeader(LogRecord record) {
6868
var maybeSplit = record.level >= Level.WARNING ? '\n' : '';
69-
return verbose || record.loggerName.contains(' ')
69+
return record.loggerName.contains(' ')
7070
? '${record.loggerName}:$maybeSplit'
7171
: '';
7272
}

build_runner_core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
`AssetWriterSpy` functionality.
2121
- Add `NodeType` to `AssetNode`, remove subtypes. Make mutations explicit.
2222
- Use `built_value` for `AssetNode` and related types, and for serialization.
23+
- Add details of what changed and what is built to `--verbose` logging.
2324

2425
## 8.0.0
2526

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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:build/build.dart';
6+
import 'package:built_collection/built_collection.dart';
7+
import 'package:watcher/watcher.dart';
8+
9+
import '../logging/log_renderer.dart';
10+
11+
/// Asset updates for an incremental build.
12+
class AssetUpdates {
13+
final BuiltSet<AssetId> added;
14+
final BuiltSet<AssetId> modified;
15+
final BuiltSet<AssetId> removed;
16+
17+
AssetUpdates({
18+
required this.added,
19+
required this.modified,
20+
required this.removed,
21+
});
22+
23+
factory AssetUpdates.from(Map<AssetId, ChangeType> updates) {
24+
final added = <AssetId>[];
25+
final modified = <AssetId>[];
26+
final removed = <AssetId>[];
27+
for (final update in updates.entries) {
28+
switch (update.value) {
29+
case ChangeType.ADD:
30+
added.add(update.key);
31+
break;
32+
case ChangeType.MODIFY:
33+
modified.add(update.key);
34+
break;
35+
case ChangeType.REMOVE:
36+
removed.add(update.key);
37+
break;
38+
}
39+
}
40+
return AssetUpdates(
41+
added: (added..sort()).toBuiltSet(),
42+
modified: (modified..sort()).toBuiltSet(),
43+
removed: (removed..sort()).toBuiltSet(),
44+
);
45+
}
46+
47+
bool get isEmpty => added.isEmpty && modified.isEmpty && removed.isEmpty;
48+
49+
/// Explain what was updated, for logging.
50+
String render(LogRenderer renderer) {
51+
if (isEmpty) {
52+
return 'Updates: none.';
53+
}
54+
55+
final result = <String>[];
56+
if (added.isNotEmpty) {
57+
result.add('added ${renderer.trimmedIds(added)}');
58+
}
59+
if (modified.isNotEmpty) {
60+
result.add('modified ${renderer.trimmedIds(modified)}');
61+
}
62+
if (removed.isNotEmpty) {
63+
result.add('removed ${renderer.trimmedIds(removed)}');
64+
}
65+
66+
return 'Updates: ${result.join('; ')}.';
67+
}
68+
}

build_runner_core/lib/src/generate/build.dart

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import '../asset/writer.dart';
2020
import '../asset_graph/graph.dart';
2121
import '../asset_graph/node.dart';
2222
import '../asset_graph/optional_output_tracker.dart';
23+
import '../changes/asset_updates.dart';
2324
import '../environment/build_environment.dart';
2425
import '../logging/build_for_input_logger.dart';
2526
import '../logging/failure_reporter.dart';
2627
import '../logging/human_readable_duration.dart';
28+
import '../logging/log_renderer.dart';
2729
import '../logging/logging.dart';
2830
import '../performance_tracking/performance_tracking_resolvers.dart';
2931
import '../util/build_dirs.dart';
@@ -56,8 +58,10 @@ class Build {
5658
final RunnerAssetWriter deleteWriter;
5759

5860
// Logging.
59-
final BuildPerformanceTracker _performanceTracker;
61+
final LogRenderer renderer;
62+
final BuildPerformanceTracker performanceTracker;
6063
late final HungActionsHeartbeat hungActionsHeartbeat;
64+
final bool logFine;
6165

6266
// State.
6367
final AssetGraph assetGraph;
@@ -78,10 +82,12 @@ class Build {
7882
required this.deleteWriter,
7983
required this.resourceManager,
8084
required this.assetGraph,
81-
}) : _performanceTracker =
85+
}) : renderer = LogRenderer(rootPackageName: options.packageGraph.root.name),
86+
performanceTracker =
8287
options.trackPerformance
8388
? BuildPerformanceTracker()
84-
: BuildPerformanceTracker.noOp() {
89+
: BuildPerformanceTracker.noOp(),
90+
logFine = _logger.level <= Level.FINE {
8591
hungActionsHeartbeat = HungActionsHeartbeat(() {
8692
final message = StringBuffer();
8793
const actionsToLogMax = 5;
@@ -101,6 +107,10 @@ class Build {
101107
}
102108

103109
Future<BuildResult> run(Map<AssetId, ChangeType> updates) async {
110+
if (logFine) {
111+
_logger.fine(AssetUpdates.from(updates).render(renderer));
112+
}
113+
104114
var watch = Stopwatch()..start();
105115
var result = await _safeBuild(updates);
106116
var optionalOutputTracker = OptionalOutputTracker(
@@ -164,7 +174,6 @@ class Build {
164174
/// capturing.
165175
Future<BuildResult> _safeBuild(Map<AssetId, ChangeType> updates) {
166176
var done = Completer<BuildResult>();
167-
168177
var heartbeat = HeartbeatLogger(
169178
transformLog: (original) => '$original, ${_buildProgress()}',
170179
waitDuration: const Duration(seconds: 1),
@@ -238,13 +247,13 @@ class Build {
238247
/// Runs the actions in [buildPhases] and returns a future which completes
239248
/// to the [BuildResult] once all [BuildPhase]s are done.
240249
Future<BuildResult> _runPhases() {
241-
return _performanceTracker.track(() async {
250+
return performanceTracker.track(() async {
242251
final outputs = <AssetId>[];
243252
for (var phaseNum = 0; phaseNum < buildPhases.length; phaseNum++) {
244253
var phase = buildPhases[phaseNum];
245254
if (phase.isOptional) continue;
246255
outputs.addAll(
247-
await _performanceTracker.trackBuildPhase(phase, () async {
256+
await performanceTracker.trackBuildPhase(phase, () async {
248257
if (phase is InBuildPhase) {
249258
var primaryInputs = await _matchingPrimaryInputs(
250259
phase.package,
@@ -268,7 +277,7 @@ class Build {
268277
return BuildResult(
269278
BuildStatus.success,
270279
outputs,
271-
performance: _performanceTracker,
280+
performance: performanceTracker,
272281
);
273282
});
274283
}
@@ -392,7 +401,7 @@ class Build {
392401
AssetId input,
393402
) async {
394403
final builder = phase.builder;
395-
var tracker = _performanceTracker.addBuilderAction(
404+
var tracker = performanceTracker.addBuilderAction(
396405
input,
397406
phase.builderLabel,
398407
);
@@ -433,7 +442,7 @@ class Build {
433442

434443
if (!await tracker.trackStage(
435444
'Setup',
436-
() => _buildShouldRun(builderOutputs, readerWriter),
445+
() => _buildShouldRun(input, builderOutputs, readerWriter),
437446
)) {
438447
return <AssetId>[];
439448
}
@@ -667,6 +676,7 @@ class Build {
667676

668677
/// Checks and returns whether any [outputs] need to be updated.
669678
Future<bool> _buildShouldRun(
679+
AssetId input,
670680
Iterable<AssetId> outputs,
671681
AssetReader reader,
672682
) async {
@@ -682,6 +692,12 @@ class Build {
682692
for (var output in outputs.skip(1)) {
683693
if (assetGraph.get(output)!.generatedNodeState!.pendingBuildAction ==
684694
PendingBuildAction.build) {
695+
if (logFine) {
696+
_logger.fine(
697+
'Build ${renderer.build(input, outputs)} because '
698+
'${renderer.id(output)} was marked for build.',
699+
);
700+
}
685701
return true;
686702
}
687703
}
@@ -713,19 +729,48 @@ class Build {
713729

714730
// Early bail out condition, this is a forced update.
715731
if (firstNodeState.pendingBuildAction == PendingBuildAction.build) {
732+
if (logFine) {
733+
_logger.fine(
734+
'Build ${renderer.build(input, outputs)} because '
735+
'${renderer.id(firstNode.id)} was marked for build.',
736+
);
737+
}
716738
return true;
717739
}
718740
// This is a fresh build or the first time we've seen this output.
719-
if (firstNodeState.previousInputsDigest == null) return true;
741+
if (firstNodeState.previousInputsDigest == null) {
742+
if (logFine) {
743+
_logger.fine(
744+
'Build ${renderer.build(input, outputs)} because '
745+
'${renderer.id(firstNode.id)} has no previousInputsDigest.',
746+
);
747+
}
748+
return true;
749+
}
720750

721751
var digest = await _computeCombinedDigest(
722752
firstNodeState.inputs,
723753
firstNode.generatedNodeConfiguration!.builderOptionsId,
724754
reader,
725755
);
726756
if (digest != firstNodeState.previousInputsDigest) {
757+
if (logFine) {
758+
_logger.fine(
759+
'Build ${renderer.build(input, outputs)} because '
760+
'inputs digest changed '
761+
'from ${renderer.digest(firstNodeState.previousInputsDigest)} '
762+
'to ${renderer.digest(digest)}.',
763+
);
764+
}
727765
return true;
728766
} else {
767+
if (logFine) {
768+
_logger.fine(
769+
'Skip bulding ${renderer.build(input, outputs)} because '
770+
'inputs digest is still '
771+
'${renderer.digest(firstNodeState.previousInputsDigest)}.',
772+
);
773+
}
729774
// Make sure to update the `state` field for all outputs.
730775
for (var id in outputs) {
731776
assetGraph.updateNode(id, (nodeBuilder) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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:build/build.dart';
6+
import 'package:crypto/crypto.dart';
7+
8+
/// Renders objects to strings for display in logs.
9+
class LogRenderer {
10+
final String rootPackageName;
11+
12+
LogRenderer({required this.rootPackageName});
13+
14+
/// Renders [id].
15+
///
16+
/// Like `AssetId.toString`, except the package name is omitted if it matches
17+
/// [rootPackageName].
18+
String id(AssetId id) {
19+
if (id.package == rootPackageName) {
20+
return id.path;
21+
} else {
22+
return id.toString();
23+
}
24+
}
25+
26+
/// Renders a build action with [input] and [outputs].
27+
///
28+
/// Output names that mostly match [input] are shortened to remove the
29+
/// duplication.
30+
///
31+
/// Examples: `foo.dart->.g.dart`, `foo.dart->(.g.dart, .other.dart)`
32+
String build(AssetId input, Iterable<AssetId> outputs) {
33+
final displayedInput = id(input);
34+
// If the input ends in `.dart`, take the base name and trim it from all
35+
// outputs. So for example `foo.dart->foo.g.dart` is shortened to
36+
// `foo.dart->.g.dart`.
37+
final trimPrefix =
38+
displayedInput.endsWith('.dart')
39+
? displayedInput.substring(0, displayedInput.length - 5)
40+
: null;
41+
final displayedOutputs =
42+
outputs.length == 1
43+
? _trim(id(outputs.first), trimPrefix: trimPrefix)
44+
: '(${trimmedIds(outputs, trimPrefix: trimPrefix)})';
45+
return '$displayedInput->$displayedOutputs';
46+
}
47+
48+
/// Returns [ids] converted to `String` as a comma-separated list,
49+
/// with all ids beyond the third replaced by "(# more)".
50+
///
51+
/// If [trimPrefix] is passed, trim the specified prefix from all items where
52+
/// it matches.
53+
String trimmedIds(Iterable<AssetId> ids, {String? trimPrefix}) {
54+
final firstThree = ids
55+
.map(id)
56+
.take(3)
57+
.map((s) => _trim(s, trimPrefix: trimPrefix));
58+
if (firstThree.length == ids.length) {
59+
return firstThree.join(', ');
60+
} else {
61+
return [...firstThree, '(${ids.length - 3} more)'].join(', ');
62+
}
63+
}
64+
65+
/// Renders [digest].
66+
///
67+
/// Returns `none` for null, or the first seven characters of the digest.
68+
String digest(Digest? digest) {
69+
if (digest == null) return 'none';
70+
var result = digest.toString();
71+
if (result.length > 7) result = result.substring(0, 7);
72+
return result;
73+
}
74+
75+
/// Returns [string] with [trimPrefix] removed if it matches.
76+
static String _trim(String string, {String? trimPrefix}) {
77+
if (trimPrefix == null) return string;
78+
if (string.startsWith(trimPrefix)) {
79+
return string.substring(trimPrefix.length);
80+
}
81+
return string;
82+
}
83+
}

0 commit comments

Comments
 (0)