Skip to content
Open
46 changes: 38 additions & 8 deletions cli/unstable_progress_bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,14 @@ function getUnitEntry(max: number): [Unit, number] {
* await bar.stop();
*/
export class ProgressBar {
#hasUpdate = true;
Copy link
Member

Choose a reason for hiding this comment

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

How about using throttle from @std/async/unstable-throttle to throttle the too many calls? That has no delay when the print call is not throttled.

constructor(...) {
  ...
  this.#throttledPrint = throttle(this.#print, 100)
}

set value(...) {
  ...
  this.#throttledPrint()
}


#value: number = 0;
/**
* The current progress that has been completed.
*
* @param value Value to set.
*
* @example Usage
* ```ts no-assert
* import { ProgressBar } from "@std/cli/unstable-progress-bar";
Expand All @@ -180,9 +186,21 @@ export class ProgressBar {
* await progressBar.stop();
* ```
*/
value: number;
set value(value: number) {
if (this.#value === value) return;
this.#value = value;
this.#hasUpdate = true;
}
get value(): number {
return this.#value;
}

#max: number = 0;
/**
* The maximum progress that is expected.
*
* @param value Max to set.
*
* @example Usage
* ```ts no-assert
* import { ProgressBar } from "@std/cli/unstable-progress-bar";
Expand All @@ -195,10 +213,20 @@ export class ProgressBar {
* await progressBar.stop();
* ```
*/
max: number;
set max(value: number) {
if (this.#max === value) return;
this.#max = value;
const [unit, rate] = getUnitEntry(this.#max);
this.#unit = unit;
this.#rate = rate;
this.#hasUpdate = true;
}
get max(): number {
return this.#max;
}

#unit: Unit;
#rate: number;
#unit: Unit = "KiB";
#rate: number = 2 ** 10;
#writer: WritableStreamDefaultWriter;
#id: number;
#startTime: number;
Expand All @@ -216,9 +244,7 @@ export class ProgressBar {
*
* @param options The options to configure various settings of the progress bar.
*/
constructor(
options: ProgressBarOptions,
) {
constructor(options: ProgressBarOptions) {
const {
writable = Deno.stderr.writable,
value = 0,
Expand Down Expand Up @@ -253,13 +279,17 @@ export class ProgressBar {
this.#lastTime = this.#startTime;
this.#lastValue = this.value;

this.#id = setInterval(() => this.#print(), 1000);
this.#id = setInterval(() => this.#print(), 100);
this.#print();
}

async #print(): Promise<void> {
const currentTime = performance.now();

const delta = currentTime - this.#lastTime;
if (delta < 1000 && !this.#hasUpdate) return;
this.#hasUpdate = false;

const size = this.value / this.max * this.#barLength | 0;
const fillChars = this.#fillChar.repeat(size);
const emptyChars = this.#emptyChar.repeat(this.#barLength - size);
Expand Down
172 changes: 152 additions & 20 deletions cli/unstable_progress_bar_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,72 @@ const decoder = new TextDecoder();

Deno.test("ProgressBar() outputs default result", async () => {
const { readable, writable } = new TransformStream();

const bar = new ProgressBar({ writable, max: 10 * 1000 });
for (let index = 0; index < 10; index++) {
bar.value += 1000;
await new Promise((resolve) => setTimeout(resolve, 100));
}
bar.stop().then(() => writable.close());

for await (const a of getData(10, 1000)) bar.value += a.length;
const expected = [
"\r\u001b[K[00:00] [--------------------------------------------------] [0.00/9.77 KiB]",
"\r\u001b[K[00:00] [#####---------------------------------------------] [0.98/9.77 KiB]",
"\r\u001b[K[00:00] [##########----------------------------------------] [1.95/9.77 KiB]",
"\r\u001b[K[00:00] [###############-----------------------------------] [2.93/9.77 KiB]",
"\r\u001b[K[00:00] [####################------------------------------] [3.91/9.77 KiB]",
"\r\u001b[K[00:00] [#########################-------------------------] [4.88/9.77 KiB]",
"\r\u001b[K[00:00] [##############################--------------------] [5.86/9.77 KiB]",
"\r\u001b[K[00:00] [###################################---------------] [6.84/9.77 KiB]",
"\r\u001b[K[00:00] [########################################----------] [7.81/9.77 KiB]",
"\r\u001b[K[00:00] [#############################################-----] [8.79/9.77 KiB]",
"\r\u001b[K[00:01] [##################################################] [9.77/9.77 KiB]",
"\n",
];

const actual = [];
for await (const buffer of readable) {
actual.push(decoder.decode(buffer));
}
assertEquals(actual, expected);
});

Deno.test("ProgressBar() outputs passing time", async () => {
const { readable, writable } = new TransformStream();

const bar = new ProgressBar({ writable, max: 10 * 1000 });
await new Promise((resolve) => setTimeout(resolve, 3000));
bar.stop().then(() => writable.close());

const expected = [
"\r\u001b[K[00:00] [--------------------------------------------------] [0.00/9.77 KiB]",
"\r\u001b[K[00:01] [--------------------------------------------------] [0.00/9.77 KiB]",
"\r\u001b[K[00:02] [--------------------------------------------------] [0.00/9.77 KiB]",
"\n",
];

const actual = [];
for await (const buffer of readable) {
actual.push(decoder.decode(buffer));
}
assertEquals(actual, expected);
});

Deno.test("ProgressBar() change max", async () => {
const { readable, writable } = new TransformStream();

const bar = new ProgressBar({ writable, max: 2 ** 10 });
bar.max = 2 ** 20;
await new Promise((resolve) => setTimeout(resolve, 100));
bar.stop().then(() => writable.close());

const expected = [
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/9.77 KiB]",
"\r\x1b[K[00:00] [##################################################] [9.77/9.77 KiB]",
"\r\u001b[K[00:00] [--------------------------------------------------] [0.00/1.00 KiB]",
"\r\u001b[K[00:00] [--------------------------------------------------] [0.00/1.00 MiB]",
"\n",
];

const actual: string[] = [];
const actual = [];
for await (const buffer of readable) {
actual.push(decoder.decode(buffer));
}
Expand All @@ -37,27 +91,42 @@ Deno.test("ProgressBar() outputs default result", async () => {

Deno.test("ProgressBar() can handle a readable.cancel() correctly", async () => {
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({ writable, max: 10 * 1000 });

for await (const a of getData(10, 1000)) bar.value += a.length;
const bar = new ProgressBar({ writable, max: 10 * 1000 });
for (let index = 0; index < 10; index++) {
bar.value += 1000;
await new Promise((resolve) => setTimeout(resolve, 100));
}
bar.stop();

await readable.cancel();
});

Deno.test("ProgressBar() can remove itself when finished", async () => {
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({ writable, max: 10 * 1000, clear: true });

for await (const a of getData(10, 1000)) bar.value += a.length;
const bar = new ProgressBar({ writable, max: 10 * 1000, clear: true });
for (let index = 0; index < 10; index++) {
bar.value += 1000;
await new Promise((resolve) => setTimeout(resolve, 100));
}
bar.stop().then(() => writable.close());

const expected = [
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/9.77 KiB]",
"\r\u001b[K[00:00] [--------------------------------------------------] [0.00/9.77 KiB]",
"\r\u001b[K[00:00] [#####---------------------------------------------] [0.98/9.77 KiB]",
"\r\u001b[K[00:00] [##########----------------------------------------] [1.95/9.77 KiB]",
"\r\u001b[K[00:00] [###############-----------------------------------] [2.93/9.77 KiB]",
"\r\u001b[K[00:00] [####################------------------------------] [3.91/9.77 KiB]",
"\r\u001b[K[00:00] [#########################-------------------------] [4.88/9.77 KiB]",
"\r\u001b[K[00:00] [##############################--------------------] [5.86/9.77 KiB]",
"\r\u001b[K[00:00] [###################################---------------] [6.84/9.77 KiB]",
"\r\u001b[K[00:00] [########################################----------] [7.81/9.77 KiB]",
"\r\u001b[K[00:00] [#############################################-----] [8.79/9.77 KiB]",
"\r\u001b[K[00:01] [##################################################] [9.77/9.77 KiB]",
"\r\x1b[K",
];

const actual: string[] = [];
const actual = [];
for await (const buffer of readable) {
actual.push(decoder.decode(buffer));
}
Expand Down Expand Up @@ -87,24 +156,87 @@ Deno.test("ProgressBar() passes correct values to formatter", async () => {
await new Response(readable).bytes();
});

Deno.test("ProgressBar() uses correct unit type", async () => {
const units = ["KiB", "MiB", "GiB", "TiB", "PiB"];
let i = 0;
for (const unit of units) {
Deno.test("ProgressBar() uses correct unit type", async (t) => {
await t.step("KiB", async () => {
const expected =
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/1.00 KiB]";
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({
writable,
max: 2 ** (10 * ++i),
max: 2 ** 10,
keepOpen: false,
});

const decoder = new TextDecoder();
for await (const buffer of readable) {
assertEquals(decoder.decode(buffer.subarray(-4, -1)), unit);
const actual = decoder.decode(buffer);
assertEquals(actual, expected);
break;
}
bar.stop();
}
});
await t.step("MiB", async () => {
const expected =
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/1.00 MiB]";
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({
writable,
max: 2 ** 20,
keepOpen: false,
});
for await (const buffer of readable) {
const actual = decoder.decode(buffer);
assertEquals(actual, expected);
break;
}
bar.stop();
});
await t.step("GiB", async () => {
const expected =
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/1.00 GiB]";
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({
writable,
max: 2 ** 30,
keepOpen: false,
});
for await (const buffer of readable) {
const actual = decoder.decode(buffer);
assertEquals(actual, expected);
break;
}
bar.stop();
});
await t.step("TiB", async () => {
const expected =
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/1.00 TiB]";
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({
writable,
max: 2 ** 40,
keepOpen: false,
});
for await (const buffer of readable) {
const actual = decoder.decode(buffer);
assertEquals(actual, expected);
break;
}
bar.stop();
});
await t.step("PiB", async () => {
const expected =
"\r\x1b[K[00:00] [--------------------------------------------------] [0.00/1.00 PiB]";
const { readable, writable } = new TransformStream();
const bar = new ProgressBar({
writable,
max: 2 ** 50,
keepOpen: false,
});
for await (const buffer of readable) {
const actual = decoder.decode(buffer);
assertEquals(actual, expected);
break;
}
bar.stop();
});
});

Deno.test("ProgressBar() does not leak resources when immediately stopped", async () => {
Expand Down
Loading