Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 04d8a28

Browse files
committed
[et] Improve the logger for the ninja build, adds a spinner
1 parent 34a8b9b commit 04d8a28

File tree

5 files changed

+319
-35
lines changed

5 files changed

+319
-35
lines changed

tools/engine_tool/lib/src/commands/build_command.dart

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
import 'package:engine_build_configs/engine_build_configs.dart';
66

77
import '../build_utils.dart';
8-
8+
import '../logger.dart';
99
import 'command.dart';
10-
11-
const String _configFlag = 'config';
10+
import 'flags.dart';
1211

1312
// TODO(johnmccutchan): Should BuildConfig be BuilderConfig and GlobalBuild be BuildConfig?
1413
// TODO(johnmccutchan): List all available build targets and allow the user
@@ -25,7 +24,7 @@ final class BuildCommand extends CommandBase {
2524
builds = runnableBuilds(environment, configs);
2625
// Add options here that are common to all queries.
2726
argParser.addOption(
28-
_configFlag,
27+
configFlag,
2928
abbr: 'c',
3029
defaultsTo: 'host_debug',
3130
help: 'Specify the build config to use',
@@ -51,7 +50,7 @@ final class BuildCommand extends CommandBase {
5150

5251
@override
5352
Future<int> run() async {
54-
final String configName = argResults![_configFlag] as String;
53+
final String configName = argResults![configFlag] as String;
5554
final GlobalBuild? build = builds
5655
.where((GlobalBuild build) => build.name == configName)
5756
.firstOrNull;
@@ -60,28 +59,38 @@ final class BuildCommand extends CommandBase {
6059
return 1;
6160
}
6261
final GlobalBuildRunner buildRunner = GlobalBuildRunner(
63-
platform: environment.platform,
64-
processRunner: environment.processRunner,
65-
abi: environment.abi,
66-
engineSrcDir: environment.engine.srcDir,
67-
build: build);
62+
platform: environment.platform,
63+
processRunner: environment.processRunner,
64+
abi: environment.abi,
65+
engineSrcDir: environment.engine.srcDir,
66+
build: build,
67+
runTests: false,
68+
);
69+
70+
Spinner? spinner;
6871
void handler(RunnerEvent event) {
6972
switch (event) {
7073
case RunnerStart():
71-
environment.logger.info('$event: ${event.command.join(' ')}');
74+
environment.logger.status('$event ', newline: false);
75+
spinner = environment.logger.startSpinner();
7276
case RunnerProgress(done: true):
77+
spinner?.finish();
78+
spinner = null;
7379
environment.logger.clearLine();
7480
environment.logger.status(event);
75-
case RunnerProgress(done: false):
76-
{
77-
final String percent = '${event.percent.toStringAsFixed(1)}%';
78-
final String fraction = '(${event.completed}/${event.total})';
79-
final String prefix = '[${event.name}] $percent $fraction ';
80-
final String what = event.what;
81-
environment.logger.clearLine();
82-
environment.logger.status('$prefix$what');
83-
}
81+
case RunnerProgress(done: false): {
82+
spinner?.finish();
83+
spinner = null;
84+
final String percent = '${event.percent.toStringAsFixed(1)}%';
85+
final String fraction = '(${event.completed}/${event.total})';
86+
final String prefix = '[${event.name}] $percent $fraction ';
87+
final String what = event.what;
88+
environment.logger.clearLine();
89+
environment.logger.status('$prefix$what', newline: false, fit: true);
90+
}
8491
default:
92+
spinner?.finish();
93+
spinner = null;
8594
environment.logger.status(event);
8695
}
8796
}

tools/engine_tool/lib/src/commands/flags.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// Keep this list alphabetized.
1414
const String allFlag = 'all';
1515
const String builderFlag = 'builder';
16+
const String configFlag = 'config';
1617
const String dryRunFlag = 'dry-run';
1718
const String quietFlag = 'quiet';
19+
const String runTestsFlag = 'run-tests';
1820
const String verboseFlag = 'verbose';

tools/engine_tool/lib/src/logger.dart

Lines changed: 209 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:async' show runZoned;
5+
import 'dart:async' show Timer, runZoned;
66
import 'dart:io' as io show
77
IOSink,
88
stderr,
@@ -29,7 +29,7 @@ import 'package:meta/meta.dart';
2929
/// which can be inspected by unit tetss.
3030
class Logger {
3131
/// Constructs a logger for use in the tool.
32-
Logger() : _logger = log.Logger.detached('et') {
32+
Logger() : _logger = log.Logger.detached('et'), _test = false {
3333
_logger.level = statusLevel;
3434
_logger.onRecord.listen(_handler);
3535
_setupIoSink(io.stderr);
@@ -38,7 +38,7 @@ class Logger {
3838

3939
/// A logger for tests.
4040
@visibleForTesting
41-
Logger.test() : _logger = log.Logger.detached('et') {
41+
Logger.test() : _logger = log.Logger.detached('et'), _test = true {
4242
_logger.level = statusLevel;
4343
_logger.onRecord.listen((log.LogRecord r) => _testLogs.add(r));
4444
}
@@ -94,6 +94,9 @@ class Logger {
9494

9595
final log.Logger _logger;
9696
final List<log.LogRecord> _testLogs = <log.LogRecord>[];
97+
final bool _test;
98+
99+
Spinner? _status;
97100

98101
/// Get the current logging level.
99102
log.Level get level => _logger.level;
@@ -104,43 +107,234 @@ class Logger {
104107
}
105108

106109
/// Record a log message at level [Logger.error].
107-
void error(Object? message, {int indent = 0, bool newline = true}) {
108-
_emitLog(errorLevel, message, indent, newline);
110+
void error(
111+
Object? message, {
112+
int indent = 0,
113+
bool newline = true,
114+
bool fit = false,
115+
}) {
116+
_emitLog(errorLevel, message, indent, newline, fit);
109117
}
110118

111119
/// Record a log message at level [Logger.warning].
112-
void warning(Object? message, {int indent = 0, bool newline = true}) {
113-
_emitLog(warningLevel, message, indent, newline);
120+
void warning(
121+
Object? message, {
122+
int indent = 0,
123+
bool newline = true,
124+
bool fit = false,
125+
}) {
126+
_emitLog(warningLevel, message, indent, newline, fit);
114127
}
115128

116129
/// Record a log message at level [Logger.warning].
117-
void status(Object? message, {int indent = 0, bool newline = true}) {
118-
_emitLog(statusLevel, message, indent, newline);
130+
void status(
131+
Object? message, {
132+
int indent = 0,
133+
bool newline = true,
134+
bool fit = false,
135+
}) {
136+
_emitLog(statusLevel, message, indent, newline, fit);
119137
}
120138

121139
/// Record a log message at level [Logger.info].
122-
void info(Object? message, {int indent = 0, bool newline = true}) {
123-
_emitLog(infoLevel, message, indent, newline);
140+
void info(
141+
Object? message, {
142+
int indent = 0,
143+
bool newline = true,
144+
bool fit = false,
145+
}) {
146+
_emitLog(infoLevel, message, indent, newline, fit);
124147
}
125148

126149
/// Writes a number of spaces to stdout equal to the width of the terminal
127150
/// and emits a carriage return.
128151
void clearLine() {
129-
if (!io.stdout.hasTerminal) {
152+
if (!io.stdout.hasTerminal || _test) {
153+
return;
154+
}
155+
_status?.pause();
156+
_emitClearLine();
157+
_status?.resume();
158+
}
159+
160+
/// Starts printing a progress spinner.
161+
Spinner startSpinner({
162+
void Function()? onFinish,
163+
}) {
164+
void finishCallback() {
165+
onFinish?.call();
166+
_status = null;
167+
}
168+
_status = io.stdout.hasTerminal && !_test
169+
? FlutterSpinner(onFinish: finishCallback)
170+
: Spinner(onFinish: finishCallback);
171+
_status!.start();
172+
return _status!;
173+
}
174+
175+
static void _emitClearLine() {
176+
if (io.stdout.supportsAnsiEscapes) {
177+
// Go to start of the line and clear the line.
178+
_ioSinkWrite(io.stdout, '\r\x1B[K');
130179
return;
131180
}
132181
final int width = io.stdout.terminalColumns;
182+
final String backspaces = '\b' * width;
133183
final String spaces = ' ' * width;
134-
_ioSinkWrite(io.stdout, '$spaces\r');
184+
_ioSinkWrite(io.stdout, '$backspaces$spaces$backspaces');
135185
}
136186

137-
void _emitLog(log.Level level, Object? message, int indent, bool newline) {
138-
final String m = '${' ' * indent}$message${newline ? '\n' : ''}';
187+
void _emitLog(
188+
log.Level level,
189+
Object? message,
190+
int indent,
191+
bool newline,
192+
bool fit,
193+
) {
194+
String m = '${' ' * indent}$message${newline ? '\n' : ''}';
195+
if (fit && io.stdout.hasTerminal) {
196+
m = fitToWidth(m, io.stdout.terminalColumns);
197+
}
198+
_status?.pause();
139199
_logger.log(level, m);
200+
_status?.resume();
201+
}
202+
203+
/// Shorten a string such that its length will be `w` by replacing
204+
/// enough characters in the middle with '...'. Trailing whitespace will not
205+
/// be preserved or counted against 'w', but if the input ends with a newline,
206+
/// then the output will end with a newline that is not counted against 'w'.
207+
/// That is, if the input string ends with a newline, the output string will
208+
/// have length up to (w + 1) and end with a newline.
209+
///
210+
/// If w <= 0, the result will be the empty string.
211+
/// If w <= 3, the result will be a string containing w '.'s.
212+
/// If there are a different number of non-'...' characters to the right and
213+
/// left of '...' in the result, then the right will have one more than the
214+
/// left.
215+
@visibleForTesting
216+
static String fitToWidth(String s, int w) {
217+
// Preserve a trailing newline if needed.
218+
final String maybeNewline = s.endsWith('\n') ? '\n' : '';
219+
if (w <= 0) {
220+
return maybeNewline;
221+
}
222+
if (w <= 3) {
223+
return '${'.' * w}$maybeNewline';
224+
}
225+
226+
// But remove trailing whitespace before removing the middle of the string.
227+
s = s.trimRight();
228+
if (s.length <= w) {
229+
return '$s$maybeNewline';
230+
}
231+
232+
// remove (s.length + 3 - w) characters from the middle of `s` and
233+
// replace them with '...'.
234+
final int diff = (s.length + 3) - w;
235+
final int leftEnd = (s.length - diff) ~/ 2;
236+
final int rightStart = (s.length + diff) ~/ 2;
237+
s = s.replaceRange(leftEnd, rightStart, '...');
238+
return s + maybeNewline;
140239
}
141240

142241
/// In a [Logger] constructed by [Logger.test], this list will contain all of
143242
/// the [LogRecord]s emitted by the test.
144243
@visibleForTesting
145244
List<log.LogRecord> get testLogs => _testLogs;
146245
}
246+
247+
248+
/// A base class for progress spinners, and a no-op implementation that prints
249+
/// nothing.
250+
class Spinner {
251+
/// Creates a progress spinner. If supplied the `onDone` callback will be
252+
/// called when `finish()` is called.
253+
Spinner({
254+
this.onFinish,
255+
});
256+
257+
/// The callback called when `finish()` is called.
258+
final void Function()? onFinish;
259+
260+
/// Starts the spinner animation.
261+
void start() {}
262+
263+
/// Pauses the spinner animation. That is, this call causes printing to the
264+
/// terminal to stop.
265+
void pause() {}
266+
267+
/// Resumes the animation at the same from where `pause()` was called.
268+
void resume() {}
269+
270+
/// Ends an animation, calling the `onFinish` callback if one was provided.
271+
void finish() {
272+
onFinish?.call();
273+
}
274+
}
275+
276+
/// A [Spinner] implementation that prints an animated "Flutter" banner.
277+
class FlutterSpinner extends Spinner {
278+
// ignore: public_member_api_docs
279+
FlutterSpinner({
280+
super.onFinish,
281+
});
282+
283+
@visibleForTesting
284+
/// The frames of the animation.
285+
static const String frames = '⢸⡯⠭⠅⢸⣇⣀⡀⢸⣇⣸⡇⠈⢹⡏⠁⠈⢹⡏⠁⢸⣯⣭⡅⢸⡯⢕⡂⠀⠀';
286+
287+
static final List<String> _flutterAnimation = frames
288+
.runes
289+
.map<String>((int scalar) => String.fromCharCode(scalar))
290+
.toList();
291+
292+
Timer? _timer;
293+
int _ticks = 0;
294+
int _lastAnimationFrameLength = 0;
295+
296+
@override
297+
void start() {
298+
_startSpinner();
299+
}
300+
301+
void _startSpinner() {
302+
_timer = Timer.periodic(const Duration(milliseconds: 100), _callback);
303+
_callback(_timer!);
304+
}
305+
306+
void _callback(Timer timer) {
307+
Logger._ioSinkWrite(io.stdout, '\b' * _lastAnimationFrameLength);
308+
_ticks += 1;
309+
final String newFrame = _currentAnimationFrame;
310+
_lastAnimationFrameLength = newFrame.runes.length;
311+
Logger._ioSinkWrite(io.stdout, newFrame);
312+
}
313+
314+
String get _currentAnimationFrame {
315+
return _flutterAnimation[_ticks % _flutterAnimation.length];
316+
}
317+
318+
@override
319+
void pause() {
320+
Logger._emitClearLine();
321+
_lastAnimationFrameLength = 0;
322+
_timer?.cancel();
323+
}
324+
325+
@override
326+
void resume() {
327+
_startSpinner();
328+
}
329+
330+
@override
331+
void finish() {
332+
_timer?.cancel();
333+
_timer = null;
334+
Logger._emitClearLine();
335+
_lastAnimationFrameLength = 0;
336+
if (onFinish != null) {
337+
onFinish!();
338+
}
339+
}
340+
}

0 commit comments

Comments
 (0)