Skip to content

Commit

Permalink
Add some progress bars for time-intensive tasks (#3579)
Browse files Browse the repository at this point in the history
  • Loading branch information
srawlins authored Nov 15, 2023
1 parent d4ab3ee commit 8f966f6
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 97 deletions.
10 changes: 3 additions & 7 deletions lib/src/dartdoc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,8 @@ class Dartdoc {

var warnings = packageGraph.packageWarningCounter.warningCount;
var errors = packageGraph.packageWarningCounter.errorCount;
if (warnings == 0 && errors == 0) {
logInfo('no issues found');
} else {
logWarning("Found $warnings ${pluralize('warning', warnings)} "
"and $errors ${pluralize('error', errors)}.");
}
logWarning("Found $warnings ${pluralize('warning', warnings)} "
"and $errors ${pluralize('error', errors)}.");

var seconds = stopwatch.elapsedMilliseconds / 1000.0;
libs = packageGraph.localPublicLibraries.length;
Expand Down Expand Up @@ -288,7 +284,7 @@ class Dartdoc {
exitCode = e is DartdocFailure ? 1 : 255;
},
zoneSpecification: ZoneSpecification(
print: (_, __, ___, String line) => logPrint(line),
print: (_, __, ___, String line) => logInfo(line),
),
);
}
Expand Down
201 changes: 152 additions & 49 deletions lib/src/logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io' show stderr, stdout;
import 'dart:io' as io;

import 'package:analyzer/file_system/file_system.dart';
import 'package:cli_util/cli_logging.dart' show Ansi;
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/package_meta.dart';
import 'package:dartdoc/src/progress_bar.dart';
import 'package:logging/logging.dart';

final _logger = Logger('dartdoc');

/// A custom [Level] for tracking file writes and verification.
///
/// Has a value of `501` – one more than [Level.FINE].
const Level progressLevel = Level('PROGRESS', 501);
const Level _progressLevel = Level('PROGRESS', 501);

/// A custom [Level] for errant print statements.
///
Expand All @@ -36,13 +36,37 @@ void logDebug(String message) {
}

void logProgress(String message) {
_logger.log(progressLevel, message);
_logger.log(_progressLevel, message);
}

void logPrint(String message) {
_logger.log(printLevel, message);
}

/// Creates a new deterministic progress bar, and displays it (with zero
/// progress).
void progressBarStart(int totalTickCount) {
_DartdocLogger.instance.progressBarStart(totalTickCount);
}

/// Increments the progress of the current progress bar.
void progressBarTick() {
_DartdocLogger.instance.progressBarTick();
}

/// Updates the total length of the current progress bar.
void progressBarUpdateTickCount(int totalTickCount) {
_DartdocLogger.instance.progressBarUpdateTickCount(totalTickCount);
}

/// Completes the current progress bar.
///
/// It is important to call this after progress is complete, in case rounding
/// errors leave the displayed progress bar at something less than 100%.
void progressBarComplete() {
_DartdocLogger.instance.progressBarComplete();
}

abstract class Jsonable {
/// The `String` to print when in human-readable mode
String get text;
Expand All @@ -54,47 +78,61 @@ abstract class Jsonable {
String toString() => text;
}

void startLogging(LoggingContext config) {
// By default, get all log output at `progressLevel` or greater.
// This allows us to capture progress events and print `...`.
// Change this to `Level.FINE` for debug logging.
Logger.root.level = progressLevel;
if (config.json) {
Logger.root.onRecord.listen((record) {
if (record.level == progressLevel) {
return;
}
class _DartdocLogger {
/// By default, we use a quiet logger.
///
/// This field can be re-set, with [startLogging].
static _DartdocLogger instance =
_DartdocLogger._(isJson: false, isQuiet: true, showProgress: false);

var output = <String, dynamic>{'level': record.level.name};
final bool _showProgressBar;

if (record.object is Jsonable) {
output['data'] = record.object;
} else {
output['message'] = record.message;
}
ProgressBar? _progressBar;

print(json.encode(output));
});
} else {
_DartdocLogger._({
required bool isJson,
required bool isQuiet,
required bool showProgress,
}) : _showProgressBar = showProgress && !isJson && !isQuiet {
// By default, get all log output at `progressLevel` or greater.
// This allows us to capture progress events and print `...`.
// Change this to `Level.FINE` for debug logging.
Logger.root.level = _progressLevel;
if (isJson) {
Logger.root.onRecord.listen(_onJsonRecord);
return;
}

_initialize(isQuiet: isQuiet, showProgress: showProgress);
}

/// Initializes this as a non-JSON logger.
///
/// This method mostly sets up callback behavior for each logged message.
void _initialize({required bool isQuiet, required bool showProgress}) {
final stopwatch = Stopwatch()..start();

// Used to track if we're printing `...` to show progress.
// Allows unified new-line tracking
var writingProgress = false;
var ansi = Ansi(Ansi.terminalSupportsAnsi);
var spinnerIndex = 0;
const spinner = ['-', r'\', '|', '/'];

Logger.root.onRecord.listen((record) {
if (record.level == progressLevel) {
if (!config.quiet &&
config.showProgress &&
if (record.level == progressBarUpdate) {
io.stdout.write(record.message);
return;
}

if (record.level == _progressLevel) {
if (!isQuiet &&
showProgress &&
stopwatch.elapsed.inMilliseconds > 125) {
if (writingProgress = false) {
stdout.write(' ');
io.stdout.write(' ');
}
writingProgress = true;
stdout.write('${ansi.backspace}${spinner[spinnerIndex]}');
io.stdout.write('$_backspace${spinner[spinnerIndex]}');
spinnerIndex = (spinnerIndex + 1) % spinner.length;
stopwatch.reset();
}
Expand All @@ -103,26 +141,79 @@ void startLogging(LoggingContext config) {

stopwatch.reset();
if (writingProgress) {
stdout.write('${ansi.backspace} ${ansi.backspace}');
io.stdout.write('$_backspace $_backspace');
}
var message = record.message;
assert(message.isNotEmpty);

if (record.level < Level.WARNING) {
if (!config.quiet) {
if (!isQuiet) {
print(message);
}
} else {
if (writingProgress) {
// Some console implementations, like IntelliJ, apparently need
// the backspace to occur for stderr as well.
stderr.write('${ansi.backspace} ${ansi.backspace}');
io.stderr.write('$_backspace $_backspace');
}
stderr.writeln(message);
io.stderr.writeln(message);
}
writingProgress = false;
});
}

void progressBarStart(int totalTickCount) {
if (!_showProgressBar) {
return;
}
_progressBar = ProgressBar(_logger, totalTickCount);
}

void progressBarTick() {
if (!_showProgressBar) {
return;
}
_progressBar?.tick();
}

void progressBarUpdateTickCount(int totalTickCount) {
if (!_showProgressBar) {
return;
}
_progressBar?.totalTickCount = totalTickCount;
}

void progressBarComplete() {
if (!_showProgressBar) {
return;
}
_progressBar?.complete();
_progressBar = null;
}

void _onJsonRecord(LogRecord record) {
if (record.level == _progressLevel) {
return;
}

var output = <String, dynamic>{'level': record.level.name};

if (record.object is Jsonable) {
output['data'] = record.object;
} else {
output['message'] = record.message;
}

print(json.encode(output));
}
}

void startLogging(LoggingContext config) {
_DartdocLogger.instance = _DartdocLogger._(
isJson: config.json,
isQuiet: config.quiet,
showProgress: config.showProgress,
);
}

mixin LoggingContext on DartdocOptionContextBase {
Expand All @@ -137,22 +228,34 @@ List<DartdocOption<Object>> createLoggingOptions(
PackageMetaProvider packageMetaProvider) {
var resourceProvider = packageMetaProvider.resourceProvider;
return [
DartdocOptionArgOnly<bool>('json', false, resourceProvider,
help: 'Prints out progress JSON maps. One entry per line.',
negatable: true),
DartdocOptionArgOnly<bool>(
'showProgress', Ansi.terminalSupportsAnsi, resourceProvider,
help: 'Display progress indications to console stdout.',
negatable: true),
DartdocOptionArgSynth<bool>('quiet',
(DartdocSyntheticOption<Object> option, Folder dir) {
if (option.parent['generateDocs'].valueAt(dir) == false) {
return true;
}
return false;
}, resourceProvider,
abbr: 'q',
negatable: true,
help: 'Only show warnings and errors; silence all other output.'),
'json',
false,
resourceProvider,
help: 'Prints out progress JSON maps. One entry per line.',
negatable: true,
),
DartdocOptionArgOnly<bool>(
'showProgress',
_terminalSupportsAnsi,
resourceProvider,
help: 'Display progress indications to console stdout.',
negatable: true,
),
DartdocOptionArgSynth<bool>(
'quiet',
(DartdocSyntheticOption<Object> option, Folder dir) =>
option.parent['generateDocs'].valueAt(dir) == false,
resourceProvider,
abbr: 'q',
negatable: true,
help: 'Only show warnings and errors; silence all other output.',
),
];
}

const String _backspace = '\b';

bool get _terminalSupportsAnsi =>
io.stdout.supportsAnsiEscapes &&
io.stdioType(io.stdout) == io.StdioType.terminal;
15 changes: 13 additions & 2 deletions lib/src/model/package_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,15 @@ class PubPackageBuilder implements PackageBuilder {
// find all documentable files in that package, for the universal reference
// scope. This variable tracks which packages we've seen so far.
var knownPackages = <PackageMeta>{};
if (!addingSpecials) {
progressBarStart(files.length);
}
do {
filesInLastPass = filesInCurrentPass;
var newFiles = <String>{};
if (!addingSpecials) {
progressBarUpdateTickCount(files.length);
}
// Be careful here, not to accidentally stack up multiple
// [DartDocResolvedLibrary]s, as those eat our heap.
var libraryFiles = files.difference(_knownParts);
Expand All @@ -239,7 +245,9 @@ class PubPackageBuilder implements PackageBuilder {
continue;
}
processedFiles.add(file);
logProgress(file);
if (!addingSpecials) {
progressBarTick();
}
var resolvedLibrary = await _resolveLibrary(file);
if (resolvedLibrary == null) {
_knownParts.add(file);
Expand Down Expand Up @@ -283,6 +291,9 @@ class PubPackageBuilder implements PackageBuilder {
knownPackages.addAll(packages);
}
} while (!filesInLastPass.containsAll(filesInCurrentPass));
if (!addingSpecials) {
progressBarComplete();
}
}

/// Whether [libraryElement] should be included in the libraries-to-document.
Expand Down Expand Up @@ -439,7 +450,7 @@ class PubPackageBuilder implements PackageBuilder {
var files = await _getFilesToDocument();
var specialFiles = specialLibraryFiles(findSpecialsSdk);

logDebug('${DateTime.now()}: Discovering Dart libraries...');
logInfo('Discovering libraries...');
var foundLibraries = <LibraryElement>{};
await _discoverLibraries(
uninitializedPackageGraph.addLibraryToGraph,
Expand Down
Loading

0 comments on commit 8f966f6

Please sign in to comment.