From 911ca32c35e2434b42122a23a8cfc2c49c0b3d2c Mon Sep 17 00:00:00 2001 From: Jason Miller Date: Mon, 5 Oct 2020 22:51:23 -0400 Subject: [PATCH] Fancy progress output --- cli/package.json | 2 + cli/src/index.js | 99 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 23 deletions(-) diff --git a/cli/package.json b/cli/package.json index ed8ce54f..4053e67d 100644 --- a/cli/package.json +++ b/cli/package.json @@ -24,6 +24,8 @@ "@rollup/plugin-node-resolve": "^9.0.0", "commander": "^6.0.0", "json5": "^2.1.3", + "kleur": "^4.1.3", + "ora": "^5.1.0", "rollup": "^2.26.11", "rollup-plugin-terser": "^7.0.2" } diff --git a/cli/src/index.js b/cli/src/index.js index 8ee936d0..45941a48 100644 --- a/cli/src/index.js +++ b/cli/src/index.js @@ -5,6 +5,8 @@ import { cpus } from "os"; import { extname, join, basename } from "path"; import { promises as fsp } from "fs"; import { version } from "json:../package.json"; +import ora from 'ora'; +import kleur from 'kleur'; import supportedFormats from "./codecs.js"; import WorkerPool from "./worker_pool.js"; @@ -55,7 +57,7 @@ async function encodeFile({ optimizerButteraugliTarget, maxOptimizerRounds }) { - let out; + let out, infoText; const encoder = await supportedFormats[encName].enc(); if (encConfig === "auto") { const optionToOptimize = supportedFormats[encName].autoOptimize.option; @@ -82,11 +84,11 @@ async function encodeFile({ } ); out = binary; - console.log( - `Used \`--${encName} '${JSON.stringify({ - [optionToOptimize]: quality - })}'\` for ${outputFile}` - ); + const opts = { + // 5 significant digits is enough + [optionToOptimize]: Math.round(quality * 10000) / 10000 + }; + infoText = ` using --${encName} '${JSON5.stringify(opts)}'`; } else { out = encoder.encode( bitmapIn.data.buffer, @@ -97,6 +99,7 @@ async function encodeFile({ } await fsp.writeFile(outputFile, out); return { + infoText, inputSize: size, inputFile: file, outputFile, @@ -114,8 +117,60 @@ function handleJob(params) { return decodeFile(params.file); } } + +function progressTracker(results) { + const spinner = ora(); + const tracker = {}; + tracker.spinner = spinner; + tracker.progressOffset = 0; + tracker.totalOffset = 0; + let status = ''; + tracker.setStatus = (text) => { + status = text || ''; + update(); + }; + let progress = ''; + tracker.setProgress = (done, total) => { + spinner.prefixText = kleur.dim(`${done}/${total}`); + const completeness = (tracker.progressOffset + done) / (tracker.totalOffset + total); + progress = kleur.cyan(`▐${'▨'.repeat(completeness*10|0).padEnd(10, '╌')}▌ `); + update(); + }; + function update() { + spinner.text = progress + kleur.bold(status) + getResultsText(); + } + tracker.finish = (text) => { + spinner.succeed(kleur.bold(text) + getResultsText()); + } + function getResultsText() { + let out = ''; + for (const [filename, result] of results.entries()) { + out += `\n ${kleur.cyan(filename)}: ${prettyPrintSize(result.size)}`; + for (const { outputFile, outputSize, infoText } of result.outputs) { + const name = (program.suffix + extname(outputFile)).padEnd(5); + out += `\n ${kleur.dim('└')} ${kleur.cyan(name)} → ${prettyPrintSize(outputSize)}`; + const percent = ((outputSize / result.size) * 100).toPrecision(3); + out += ` (${kleur[outputSize>result.size?'red':'green'](percent+'%')})`; + if (infoText) out += kleur.yellow(infoText); + } + } + return out || '\n'; + } + spinner.start(); + return tracker; +} + async function processFiles(files) { - const workerPool = new WorkerPool(cpus().length, __filename); + const parallelism = cpus().length; + + const results = new Map(); + const progress = progressTracker(results); + + progress.setStatus('Decoding...'); + progress.totalOffset = files.length; + progress.setProgress(0, files.length); + + const workerPool = new WorkerPool(parallelism, __filename); // Create output directory await fsp.mkdir(program.outputDir, { recursive: true }); @@ -131,8 +186,11 @@ async function processFiles(files) { return result; })); - const decodedFiles = await Promise.all(files.map(file => decodeFile(file))); + progress.progressOffset = decoded; + progress.setStatus('Encoding ' + kleur.dim(`(${parallelism} threads)`)); + progress.setProgress(0, files.length); + const jobs = []; let jobsStarted = 0; let jobsFinished = 0; for (const { file, bitmap, size } of decodedFiles) { @@ -155,7 +213,7 @@ async function processFiles(files) { ); const outputFile = join(program.outputDir, `${base}.${value.extension}`); jobsStarted++; - workerPool + const p = workerPool .dispatchJob({ operation: 'encode', file, @@ -167,26 +225,21 @@ async function processFiles(files) { optimizerButteraugliTarget: Number(program.optimizerButteraugliTarget), maxOptimizerRounds: Number(program.maxOptimizerRounds) }) - .then(({ outputFile, inputSize, outputSize }) => { + .then((output) => { jobsFinished++; - const numDigits = jobsStarted.toString().length; - console.log( - `${jobsFinished - .toString() - .padStart( - numDigits - )}/${jobsStarted}: ${outputFile} ${prettyPrintSize( - inputSize - )} -> ${prettyPrintSize(outputSize)} (${( - (outputSize / inputSize) * - 100 - ).toFixed(1)}%)` - ); + results.get(file).outputs.push(output); + progress.setProgress(jobsFinished, jobsStarted); }); + jobs.push(p); } } + + // update the progress to account for multi-format + progress.setProgress(jobsFinished, jobsStarted); // Wait for all jobs to finish await workerPool.join(); + await Promise.all(jobs); + progress.finish('Squoosh results:'); } if (isMainThread) {