diff --git a/.eslintrc.json b/.eslintrc.json index a095f5f..089f1f4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": ["eslint:recommended", "aenondynamics"] -} \ No newline at end of file + "extends": ["eslint:recommended", "aenondynamics"], + "parserOptions": { + "ecmaVersion": 13 + } +} diff --git a/lib/eta.js b/lib/eta.js index 4474f99..42bc18f 100644 --- a/lib/eta.js +++ b/lib/eta.js @@ -1,73 +1,115 @@ - // ETA calculation -class ETA{ +module.exports = class ETA { - constructor(length, initTime, initValue){ + // For performance this class uses arrays of numbers instead of + // objects, does not allocate memory, and does not use modulo. + constructor(length, initTime, initValue) { // size of eta buffer this.etaBufferLength = length || 100; - // eta buffer with initial values - this.valueBuffer = [initValue]; - this.timeBuffer = [initTime]; + // constant sized progress value buffer + this.valueBuffer = new Array(this.etaBufferLength); + this.valueBuffer[0] = initValue; + + // constant sized time buffer + this.timeBuffer = new Array(this.etaBufferLength); + this.timeBuffer[0] = initTime; + + // count and length of buffers + this.count = 1; + this.index = 0; // eta time value - this.eta = '0'; + this.eta = 0; + + // does eta need to be recalculated? + this.etaDirty = false; + + // last total given to update() + this.lastTotal = 0; } // add new values to calculation buffer - update(time, value, total){ - this.valueBuffer.push(value); - this.timeBuffer.push(time); + update(time, value, total) { + this.lastTotal = total; + this.etaDirty = true; + + // don't increment index if same value to prevent INF + if (value === this.valueBuffer[this.index]) { + this.timeBuffer[this.index] = time; + return; + } + + // don't increment index if same time to prevent zero + if (time === this.timeBuffer[this.index]) { + this.valueBuffer[this.index] = value; + return; + } - // trigger recalculation - this.calculate(total-value); + // increment count until buffer is filled + if (this.count < this.etaBufferLength) { + this.count++; + } + + // increment index, looping around to zero + if (++this.index === this.etaBufferLength) { + this.index = 0; + } + + // insert values + this.valueBuffer[this.index] = value; + this.timeBuffer[this.index] = time; } // fetch estimated time - getTime(){ + getTime() { + if (this.etaDirty) { + this.#calculate(); + } return this.eta; } // eta calculation - request number of remaining events - calculate(remaining){ - // get number of samples in eta buffer - const currentBufferSize = this.valueBuffer.length; - const buffer = Math.min(this.etaBufferLength, currentBufferSize); + #calculate() { + this.etaDirty = false; + + // startIndex is zero until buffers are filled, then the + // oldest values are at the next index + let startIndex = this.count < this.etaBufferLength ? 0 : this.index + 1; + if (startIndex === this.etaBufferLength) { + startIndex = 0; + } - const v_diff = this.valueBuffer[currentBufferSize - 1] - this.valueBuffer[currentBufferSize - buffer]; - const t_diff = this.timeBuffer[currentBufferSize - 1] - this.timeBuffer[currentBufferSize - buffer]; + const v_diff = this.valueBuffer[this.index] - this.valueBuffer[startIndex]; + const t_diff = this.timeBuffer[this.index] - this.timeBuffer[startIndex]; // get progress per ms const vt_rate = v_diff/t_diff; - // strip past elements - this.valueBuffer = this.valueBuffer.slice(-this.etaBufferLength); - this.timeBuffer = this.timeBuffer.slice(-this.etaBufferLength); + const remaining = this.lastTotal - this.valueBuffer[this.index]; // eq: vt_rate *x = total const eta = Math.ceil(remaining/vt_rate/1000); // check values - if (isNaN(eta)){ - this.eta = 'NULL'; + if (isNaN(eta)) { + this.eta = 'NaN'; // +/- Infinity --- NaN already handled - }else if (!isFinite(eta)){ + } else if (!isFinite(eta)) { this.eta = 'INF'; // > 10M s ? - set upper display limit ~115days (1e7/60/60/24) - }else if (eta > 1e7){ + } else if (eta > 1e7) { this.eta = 'INF'; // negative ? - }else if (eta < 0){ + } else if (eta < 0) { this.eta = 0; - }else{ + } else { // assign this.eta = eta; } } } - -module.exports = ETA; \ No newline at end of file diff --git a/test/bench.mjs b/test/bench.mjs new file mode 100644 index 0000000..4be8b3e --- /dev/null +++ b/test/bench.mjs @@ -0,0 +1,17 @@ +import * as cliProgress from '../cli-progress.js'; +import timers from 'node:timers'; + +const total = 20_000_000; +const incrementsPerYield = 1_000; +const etaBuffer = 100; + +const progress = new cliProgress.SingleBar({etaBuffer}); +progress.start(total, 0); + +console.time("duration: "); +for (let i = 0; i < total; i += incrementsPerYield) { + for (let j = 0; j < incrementsPerYield; j++) progress.increment(); + await timers.promises.scheduler.yield() +} +progress.stop(); +console.timeEnd("duration: "); diff --git a/test/lib/eta.test.js b/test/lib/eta.test.js new file mode 100644 index 0000000..03cb656 --- /dev/null +++ b/test/lib/eta.test.js @@ -0,0 +1,40 @@ +const _assert = require('assert'); +const ETA = require('../../lib/eta'); + +describe('eta', function() { + const length = 3; + const eta = new ETA(length, 0, 0); + + it('has an initial value is zero', () => { + _assert.equal(eta.getTime(), 0); + }); + + it('calculates value after updates', () => { + eta.update(1000, 1, 100); + _assert.equal(eta.getTime(), 99); + }); + + it('loops after length updates', () => { + eta.update(2000, 2, 100); + eta.update(3000, 3, 100); + _assert.equal(eta.getTime(), 97); + }); + + it('has same-value optimizations to prevent INF', () => { + eta.update(0, 3, 100); + eta.update(1000, 3, 100); + eta.update(2000, 3, 100); + eta.update(3000, 3, 100); + _assert.equal(eta.getTime(), 97); + }); + + it('has same-time optimizations to prevent zero', () => { + const eta = new ETA(length, 0, 0); + eta.update(1000, 0, 100); + eta.update(2000, 1, 100); + eta.update(3000, 2, 100); + eta.update(3000, 3, 100); + eta.update(3000, 4, 100); + _assert.equal(eta.getTime(), 48); + }); +});