Skip to content

Add some progress bars for time-intensive tasks #3579

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

Merged
merged 4 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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