Skip to content
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

fix(ng-dev): improve spinner experience when executed in a CI environment #2426

Closed
Closed
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
5 changes: 2 additions & 3 deletions ng-dev/perf/workflow/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {ChildProcess} from '../../utils/child-process.js';
import {green} from '../../utils/logging.js';
import {Spinner} from '../../utils/spinner.js';
import {Workflow} from './loader.js';

export async function measureWorkflow({name, workflow, prepare, cleanup}: Workflow) {
const spinner = new Spinner('');
const spinner = new Spinner();
try {
if (prepare) {
spinner.update('Preparing environment for workflow execution');
Expand Down Expand Up @@ -32,7 +31,7 @@ export async function measureWorkflow({name, workflow, prepare, cleanup}: Workfl

const results = performance.measure(name, 'start', 'end');

spinner.complete(` ${green('✓')} ${name}: ${results.duration.toFixed(2)}ms`);
spinner.success(`${name}: ${results.duration.toFixed(2)}ms`);

return results.toJSON();
} finally {
Expand Down
115 changes: 86 additions & 29 deletions ng-dev/utils/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,74 @@
*/

import {cursorTo, clearLine} from 'readline';
import {green, red} from './logging.js';

/** Whether execution is in a CI environment. */
const IS_CI = process.env['CI'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point in time we keep making the spinner more complicated. Should we consider just using a library here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still markedly less complicated than a library has, we can if you would like, but I do personally like the total control we have in this case. But if you feel strongly I am happy to look into options for us.

/** ANSI escape code to hide cursor in terminal. */
const hideCursor = '\x1b[?25l';
/** ANSI escape code to show cursor in terminal. */
const showCursor = '\x1b[?25h';

export class Spinner {
/** Whether the spinner is currently running. */
private isRunning = true;
/** Whether the spinner is marked as completed. */
private completed = false;
/** The id of the interval being used to trigger frame printing. */
private intervalId = setInterval(() => this.printFrame(), 125);
private intervalId = setInterval(() => this.printFrame(), IS_CI ? 2500 : 125);
/** The characters to iterate through to create the appearance of spinning in the spinner. */
private spinnerCharacters = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
/** The index of the spinner character used in the frame. */
private currentSpinnerCharacterIndex = 0;
/** The current text of the spinner. */
private text: string = '';
private _text: string = '';
private set text(text: string | undefined) {
this._text = text || this._text;
this.printFrame(this.getNextSpinnerCharacter(), text);
}
private get text(): string {
return this._text;
}

constructor();
constructor(text: string);
constructor(text?: string) {
this.hideCursor();
this.text = text;
}

/** Updates the spinner text with the provided text. */
update(text: string) {
this.text = text;
}

constructor(text: string) {
process.stdout.write(hideCursor);
this.update(text);
/** Completes the spinner marking it as successful with a `✓`. */
success(text: string): void {
this._complete(green('✓'), text);
}

/** Completes the spinner marking it as failing with an `✘`. */
failure(text: string): void {
this._complete(red('✘'), text);
}

/** Completes the spinner. */
complete() {
this._complete('', this.text);
}

/**
* Internal implementation for completing the spinner, marking it as completed, and printing the
* final frame.
*/
private _complete(prefix: string, text: string) {
if (this.completed) {
return;
}
clearInterval(this.intervalId);
this.printFrame(prefix, text);
process.stdout.write('\n');
this.showCursor();
this.completed = true;
}

/** Get the next spinner character. */
Expand All @@ -37,36 +84,46 @@ export class Spinner {
return this.spinnerCharacters[this.currentSpinnerCharacterIndex];
}

/** Print the current text for the spinner to the */
private printFrame(prefix = this.getNextSpinnerCharacter(), text = this.text) {
/**
* Print the next frame either in CI mode or local terminal mode based on whether the script is run in a
* CI environment.
*/
private printFrame(prefix = this.getNextSpinnerCharacter(), text?: string): void {
if (IS_CI) {
this.printNextCIFrame(text);
} else {
this.printNextLocalFrame(prefix, text);
}
}

/** Print the current text for the spinner to the terminal. */
private printNextLocalFrame(prefix: string, text?: string) {
cursorTo(process.stdout, 0);
process.stdout.write(` ${prefix} ${text}`);
process.stdout.write(` ${prefix} ${text || this.text}`);
// Clear to the right of the cursor location in case the new frame is shorter than the previous.
clearLine(process.stdout, 1);
cursorTo(process.stdout, 0);
}

/** Updates the spinner text with the provided text. */
update(text: string) {
this.text = text;
this.printFrame(this.spinnerCharacters[this.currentSpinnerCharacterIndex]);
/** Print the next expected piece for the spinner to stdout for CI usage. */
private printNextCIFrame(text?: string) {
if (text) {
process.stdout.write(`\n${text}.`);
return;
}
process.stdout.write('.');
}

/** Completes the spinner. */
complete(): void;
complete(text: string): void;
complete(text?: string) {
if (!this.isRunning) {
return;
/** Hide the cursor in the terminal, only executed in local environments. */
private hideCursor() {
if (!IS_CI) {
process.stdout.write(hideCursor);
}
clearInterval(this.intervalId);
clearLine(process.stdout, 1);
cursorTo(process.stdout, 0);
if (text) {
process.stdout.write(text);
process.stdout.write('\n');
}

/** Resume showing the cursor in the terminal, only executed in local environments. */
private showCursor() {
if (!IS_CI) {
process.stdout.write(showCursor);
}
process.stdout.write(showCursor);
this.isRunning = false;
}
}