From d372267a1c77bad072a43dc53032e733cb2485e5 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Mon, 22 Jul 2024 23:24:37 +0300 Subject: [PATCH 01/19] docs: fixed some typos --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 65d5d5b..6c1888b 100644 --- a/README.md +++ b/README.md @@ -7,34 +7,34 @@ ## Usage ```js -import { Recordable } from './index.js' +import { Recordable } from '@nicholaswmin/recordable' const task = new Recordable() -task.record(1) -task.record(100) +task.record(1) for (let i = 0; i < 600; i++) task.record(Math.round(Math.random() * 20) + 1) +task.record(100) console.log(task.min) // 3.05 ms console.log(task.mean) -// 11.42 ms +// 23.42 ms console.log(task.max) -// 85.17 m +// 85.17 ms console.log(task.stddev) -// 5.17 ms +// 15.17 ms ``` ### Plotting ```js const task = new Recordable() -task.record(1) -task.record(100) +task.record(1) for (let i = 0; i < 600; i++) task.record(Math.round(Math.random() * 20) + 1) +task.record(100) task.plot() ``` From 2c5a5494941b12a41882388b6ff42bd84a44420c Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 04:21:44 +0300 Subject: [PATCH 02/19] feat: added a group --- index.js | 3 +- src/colorlist.js | 21 +++++ src/recordable-group.js | 91 +++++++++++++++++++ test/index.test.js | 11 +-- .../{ => recordable}/clamped-averages.test.js | 2 +- test/{ => recordable}/constructor.test.js | 2 +- .../{ => recordable}/histogram-values.test.js | 2 +- test/{ => recordable}/histogram.test.js | 2 +- test/recordable/index.test.js | 10 ++ test/{ => recordable}/plot.test.js | 2 +- test/{ => recordable}/record-delta.test.js | 2 +- test/{ => recordable}/record.test.js | 2 +- test/{ => recordable}/reset.test.js | 2 +- test/{ => recordable}/tick.test.js | 2 +- test/{ => recordable}/to-json.test.js | 2 +- 15 files changed, 135 insertions(+), 21 deletions(-) create mode 100644 src/colorlist.js create mode 100644 src/recordable-group.js rename test/{ => recordable}/clamped-averages.test.js (98%) rename test/{ => recordable}/constructor.test.js (94%) rename test/{ => recordable}/histogram-values.test.js (96%) rename test/{ => recordable}/histogram.test.js (96%) create mode 100644 test/recordable/index.test.js rename test/{ => recordable}/plot.test.js (93%) rename test/{ => recordable}/record-delta.test.js (98%) rename test/{ => recordable}/record.test.js (96%) rename test/{ => recordable}/reset.test.js (95%) rename test/{ => recordable}/tick.test.js (85%) rename test/{ => recordable}/to-json.test.js (97%) diff --git a/index.js b/index.js index ac9f05b..cbf91a8 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ import { Recordable } from './src/recordable.js' +import { RecordableGroup } from './src/recordable-group.js' -export { Recordable } +export { Recordable, RecordableGroup } diff --git a/src/colorlist.js b/src/colorlist.js new file mode 100644 index 0000000..11b8b53 --- /dev/null +++ b/src/colorlist.js @@ -0,0 +1,21 @@ +const colorlist = { + black: '\x1B[30m', + red: '\x1B[31m', + green: '\x1B[32m', + yellow: '\x1B[33m', + blue: '\x1B[34m', + magenta: '\x1B[35m', + cyan: '\x1B[36m', + lightgray: '\x1B[37m', + default: '\x1B[39m', + darkgray: '\x1B[90m', + lightred: '\x1B[91m', + lightgreen: '\x1B[92m', + lightyellow: '\x1B[93m', + lightblue: '\x1B[94m', + lightmagenta: '\x1B[95m', + lightcyan: '\x1B[96m', + white: '\x1B[97m' +} + +export default colorlist diff --git a/src/recordable-group.js b/src/recordable-group.js new file mode 100644 index 0000000..4e79afa --- /dev/null +++ b/src/recordable-group.js @@ -0,0 +1,91 @@ +import { Recordable } from './recordable.js' +import asciichart from 'asciichart' +import colorlist from './colorlist.js' + +const colors = Object.values(colorlist) +const round = num => Math.round((num + Number.EPSILON) * 100) / 100 + +class RecordableGroup { + constructor(...args) { + Object.assign(this, args.reduce((acc, name) => { + return { ...acc, [name]: new Recordable({ name }) } + }, {})) + } + + histograms({ maxRows, sortBy, title = 'Untitled' }) { + return this.print({ + ...{ maxRows, sortBy }, + transformFn: ({ key, value }) => { + const histogram = value.histogram.toJSON() + + return { + 'count' : histogram.count, + 'min (ms)' : round(histogram.min), + 'mean (ms)' : round(histogram.mean), + 'max (ms)' : round(histogram.max), + 'dev (ms)' : round(histogram.stddev), + } + } + }) + } + + print({ + title = '', + maxRows = Infinity, + sortBy = 'pid', + transformFn = () => {} + } = {}) { + const values = Object.entries(this) + .filter(entry => entry[1] instanceof Recordable) + .sort((a, b) => b[sortBy] - a[sortBy]) + .slice(0, maxRows) + .map(([key, value]) => transformFn({ key, value })) + + console.clear() + console.log('\n', '\n') + console.log('Title:', title) + console.table(values) + + const hidden = this.getMembers().length - maxRows + + if (hidden > 1) + console.log('... plus:', hidden, 'hidden') + + console.log('\n') + } + + plot() { + const width = (process.stdout.columns || 100) - 40 + const height = (process.stdout.rows || 30) - 15 + const values = this.getMembers() + .map((m, i) => ({ + ...m, + averages: m.toClampedAverages(width), + color: colors[i % colors.length], + name: m.name + })).filter(m => m.averages.length) + + + const plot = values.map(m => m.averages).length + ? asciichart.plot(values.map(m => m.averages), { + height, colors: values.map(m => m.color) + }) : null + + console.clear() + + if (!values.length) + return console.info('not enough data to plot yet ...') + + plot + ? console.log('\n'.repeat(5), plot) + : console.info('\n'.repeat(5), 'not enough plot data yet ...') + + return plot + } + + getMembers() { + return Object.values(this).filter(value => value instanceof Recordable) + } +} + +export { RecordableGroup } diff --git a/test/index.test.js b/test/index.test.js index 395d28c..647f4f0 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,10 +1 @@ -import './constructor.test.js' -import './record-delta.test.js' -import './histogram.test.js' -import './histogram-values.test.js' -import './clamped-averages.test.js' -import './to-json.test.js' -import './record.test.js' -import './reset.test.js' -import './plot.test.js' -import './tick.test.js' +import './recordable/index.test.js' diff --git a/test/clamped-averages.test.js b/test/recordable/clamped-averages.test.js similarity index 98% rename from test/clamped-averages.test.js rename to test/recordable/clamped-averages.test.js index 15c0ae3..4e805e3 100644 --- a/test/clamped-averages.test.js +++ b/test/recordable/clamped-averages.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#toClampedAverages(maxLength)', async t => { let recordable, result = null diff --git a/test/constructor.test.js b/test/recordable/constructor.test.js similarity index 94% rename from test/constructor.test.js rename to test/recordable/constructor.test.js index 54bd16e..e537081 100644 --- a/test/constructor.test.js +++ b/test/recordable/constructor.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#constructor(name)', async t => { let recordable = null diff --git a/test/histogram-values.test.js b/test/recordable/histogram-values.test.js similarity index 96% rename from test/histogram-values.test.js rename to test/recordable/histogram-values.test.js index 0caddc0..0c1c29f 100644 --- a/test/histogram-values.test.js +++ b/test/recordable/histogram-values.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#recordable.histogram values', async t => { let recordable diff --git a/test/histogram.test.js b/test/recordable/histogram.test.js similarity index 96% rename from test/histogram.test.js rename to test/recordable/histogram.test.js index 652343a..8174567 100644 --- a/test/histogram.test.js +++ b/test/recordable/histogram.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#historicalMeans', async t => { let recordable, histogram = null diff --git a/test/recordable/index.test.js b/test/recordable/index.test.js new file mode 100644 index 0000000..395d28c --- /dev/null +++ b/test/recordable/index.test.js @@ -0,0 +1,10 @@ +import './constructor.test.js' +import './record-delta.test.js' +import './histogram.test.js' +import './histogram-values.test.js' +import './clamped-averages.test.js' +import './to-json.test.js' +import './record.test.js' +import './reset.test.js' +import './plot.test.js' +import './tick.test.js' diff --git a/test/plot.test.js b/test/recordable/plot.test.js similarity index 93% rename from test/plot.test.js rename to test/recordable/plot.test.js index 3b6a90d..26591a8 100644 --- a/test/plot.test.js +++ b/test/recordable/plot.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#plot()', async t => { let plot = null diff --git a/test/record-delta.test.js b/test/recordable/record-delta.test.js similarity index 98% rename from test/record-delta.test.js rename to test/recordable/record-delta.test.js index b87e3f6..4101383 100644 --- a/test/record-delta.test.js +++ b/test/recordable/record-delta.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' const sleep = (ms = 5) => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/test/record.test.js b/test/recordable/record.test.js similarity index 96% rename from test/record.test.js rename to test/recordable/record.test.js index b37aa13..4f65d80 100644 --- a/test/record.test.js +++ b/test/recordable/record.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#record(val)', async t => { let recordable diff --git a/test/reset.test.js b/test/recordable/reset.test.js similarity index 95% rename from test/reset.test.js rename to test/recordable/reset.test.js index 4cf2483..38da463 100644 --- a/test/reset.test.js +++ b/test/recordable/reset.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#reset()', async t => { let recordable = null diff --git a/test/tick.test.js b/test/recordable/tick.test.js similarity index 85% rename from test/tick.test.js rename to test/recordable/tick.test.js index 168c5b1..b9d22df 100644 --- a/test/tick.test.js +++ b/test/recordable/tick.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#tick()', async t => { const recordable = new Recordable() diff --git a/test/to-json.test.js b/test/recordable/to-json.test.js similarity index 97% rename from test/to-json.test.js rename to test/recordable/to-json.test.js index 0dbb2cc..1c0ef79 100644 --- a/test/to-json.test.js +++ b/test/recordable/to-json.test.js @@ -1,5 +1,5 @@ import test from 'node:test' -import { Recordable } from '../index.js' +import { Recordable } from '../../index.js' await test('#record(val)', async t => { await t.test('reimporting its JSON revives it to same state', async t => { From 90134b41f9d459ec62b71a21fe081618f8bbd507 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 09:12:31 +0300 Subject: [PATCH 03/19] feat: added groups, serialisation patches --- recordable/clamped-averages.test.js | 87 ++++++++++++++++++++++ recordable/constructor.test.js | 30 ++++++++ recordable/histogram-values.test.js | 45 +++++++++++ recordable/histogram.test.js | 53 +++++++++++++ recordable/index.test.js | 10 +++ recordable/patch-event.test.js | 39 ++++++++++ recordable/plot.test.js | 28 +++++++ recordable/record-delta.test.js | 83 +++++++++++++++++++++ recordable/record.test.js | 50 +++++++++++++ recordable/remote-patch.test.js | 63 ++++++++++++++++ recordable/reset.test.js | 36 +++++++++ recordable/tick.test.js | 13 ++++ recordable/to-json.test.js | 60 +++++++++++++++ src/recordable-group.js | 52 +++++++++---- src/recordable.js | 17 ++++- test/index.test.js | 1 + test/recordable-group/index.test.js | 2 + test/recordable-group/patch-event.test.js | 32 ++++++++ test/recordable-group/remote-patch.test.js | 84 +++++++++++++++++++++ test/recordable/patch-event.test.js | 39 ++++++++++ test/recordable/remote-patch.test.js | 63 ++++++++++++++++ 21 files changed, 871 insertions(+), 16 deletions(-) create mode 100644 recordable/clamped-averages.test.js create mode 100644 recordable/constructor.test.js create mode 100644 recordable/histogram-values.test.js create mode 100644 recordable/histogram.test.js create mode 100644 recordable/index.test.js create mode 100644 recordable/patch-event.test.js create mode 100644 recordable/plot.test.js create mode 100644 recordable/record-delta.test.js create mode 100644 recordable/record.test.js create mode 100644 recordable/remote-patch.test.js create mode 100644 recordable/reset.test.js create mode 100644 recordable/tick.test.js create mode 100644 recordable/to-json.test.js create mode 100644 test/recordable-group/index.test.js create mode 100644 test/recordable-group/patch-event.test.js create mode 100644 test/recordable-group/remote-patch.test.js create mode 100644 test/recordable/patch-event.test.js create mode 100644 test/recordable/remote-patch.test.js diff --git a/recordable/clamped-averages.test.js b/recordable/clamped-averages.test.js new file mode 100644 index 0000000..4e805e3 --- /dev/null +++ b/recordable/clamped-averages.test.js @@ -0,0 +1,87 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#toClampedAverages(maxLength)', async t => { + let recordable, result = null + + t.beforeEach(() => { + recordable = new Recordable() + }) + + await t.test('passed "maxLength"', async t => { + await t.test('is not a positive integer', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.toClampedAverages(0.01) + }, { name: 'RangeError' }) + }) + }) + + await t.test('is valid', async t => { + await t.test('has no values', async t => { + result = recordable.toClampedAverages(10) + + await t.test('returns empty array', async t => { + t.assert.strictEqual(result.length, 0) + }) + }) + + await t.test('has 1 value', async t => { + t.beforeEach(() => { + recordable.record(15) + result = recordable.toClampedAverages(10) + }) + + await t.test('returns that 1 value', async t => { + t.assert.strictEqual(result.length, 1) + t.assert.strictEqual(result.at(0), 15) + }) + }) + + await t.test('is less than total count of values', async t => { + t.beforeEach(() => { + for (let i = 1; i < 1001; i++) + recordable.record(i) + + result = recordable.toClampedAverages(10) + }) + + await t.test('returns count around maxLength', async t => { + t.assert.strictEqual(result.length, 10) + + await t.test('includes the first value', t => { + t.assert.strictEqual(result[0], 1) + }) + + await t.test('includes the last value', t => { + t.assert.strictEqual(result.at(-1), 1000) + }) + + await t.test('includes the in-between as means of values', t => { + t.assert.strictEqual(result[1], 50.5) + t.assert.strictEqual(result[4], 350.5) + t.assert.strictEqual(result[8], 750.5) + }) + }) + }) + + await t.test('is more than total count of values', async t => { + t.beforeEach(() => { + result = recordable.toClampedAverages(2000) + + for (let i = 1; i < 5 + 1; i++) + recordable.record(i) + }) + + await t.test('returns all the values', async t => { + t.assert.strictEqual(result.length, 5) + + await t.test('unchanged', t => { + t.assert.strictEqual(result.at(0), 1) + t.assert.strictEqual(result.at(-1), 5) + }) + }) + }) + }) + }) +}) diff --git a/recordable/constructor.test.js b/recordable/constructor.test.js new file mode 100644 index 0000000..e537081 --- /dev/null +++ b/recordable/constructor.test.js @@ -0,0 +1,30 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#constructor(name)', async t => { + let recordable = null + + await t.test('"name" parameter', async t => { + await t.test('is present & valid', async t => { + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('instantiates', async t => { + t.assert.ok(recordable) + + await t.test('with the name', t => { + t.assert.strictEqual(recordable.name, 'foo') + }) + }) + }) + + await t.test('is not present', async t => { + await t.test('does not throw', t => { + t.assert.doesNotThrow(() => { + recordable = new Recordable() + }) + }) + }) + }) +}) diff --git a/recordable/histogram-values.test.js b/recordable/histogram-values.test.js new file mode 100644 index 0000000..0c1c29f --- /dev/null +++ b/recordable/histogram-values.test.js @@ -0,0 +1,45 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#recordable.histogram values', async t => { + let recordable + + await t.test('object has recorded values', async t => { + t.beforeEach(() => { + recordable = new Recordable() + + recordable.record(10) + recordable.record(20) + recordable.record(30) + recordable.record(40) + recordable.record(50) + }) + + await t.test('contains properties in milliseconds', async t => { + await t.test('a min value in ms', t => { + t.assert.strictEqual(recordable.min, 10) + }) + + await t.test('a mean value in ms', t => { + t.assert.strictEqual(recordable.mean, 30) + }) + + await t.test('a max value in ms', t => { + t.assert.strictEqual(recordable.max, 50) + }) + + await t.test('a stddev value in ms', t => { + t.assert.ok(recordable.stddev > 10, recordable.stddev) + t.assert.ok(recordable.stddev < 20, recordable.stddev) + }) + + await t.test('percentiles in ms', async t => { + t.assert.ok(Object.hasOwn(recordable, 'percentiles')) + + await t.test('percentiles in ms', t => { + t.assert.ok(recordable.percentiles['100']) + }) + }) + }) + }) +}) diff --git a/recordable/histogram.test.js b/recordable/histogram.test.js new file mode 100644 index 0000000..8174567 --- /dev/null +++ b/recordable/histogram.test.js @@ -0,0 +1,53 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#historicalMeans', async t => { + let recordable, histogram = null + + t.beforeEach(() => { + recordable = new Recordable() + }) + + await t.test('instantiates', t => { + t.assert.ok(recordable, 'is falsy') + }) + + await t.test('"recordable"', async t => { + t.beforeEach(() => { + histogram = recordable.histogram + }) + + await t.test('has a histogram property', t => { + t.assert.ok(Object.hasOwn(recordable, 'histogram'), 'no such property') + }) + + await t.test('of type RecordableHistogram', t => { + t.assert.strictEqual(histogram.constructor.name, 'RecordableHistogram') + }) + + await t.test('#recordable.histogram.record(val)', async t => { + await t.test('when passed an invalid value', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + histogram.record('') + }, { name: 'TypeError' }) + }) + }) + + await t.test('when passed a valid numeric value', async t => { + t.beforeEach(() => { + histogram.record(5) + }) + + await t.test('records it', t => { + t.assert.strictEqual(recordable.count, 1) + }) + + await t.test('stores the value', t => { + t.assert.strictEqual(recordable.values.length, 1) + t.assert.ok(recordable.values.includes(5), 'cannot find 5') + }) + }) + }) + }) +}) diff --git a/recordable/index.test.js b/recordable/index.test.js new file mode 100644 index 0000000..395d28c --- /dev/null +++ b/recordable/index.test.js @@ -0,0 +1,10 @@ +import './constructor.test.js' +import './record-delta.test.js' +import './histogram.test.js' +import './histogram-values.test.js' +import './clamped-averages.test.js' +import './to-json.test.js' +import './record.test.js' +import './reset.test.js' +import './plot.test.js' +import './tick.test.js' diff --git a/recordable/patch-event.test.js b/recordable/patch-event.test.js new file mode 100644 index 0000000..0b6d90e --- /dev/null +++ b/recordable/patch-event.test.js @@ -0,0 +1,39 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#patchEvent', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('a value is recorded', async t => { + let message = null + + t.beforeEach(async () => { + recordable.ee.on('value:recorded', msg => { + message = msg + }) + + recordable.record(20) + + return new Promise(res => setTimeout(res, 50)) + }) + + await t.test('event is fired', async t => { + t.assert.ok(message) + }) + + await t.test('message look ok', async t => { + t.assert.ok(typeof message, 'object') + t.assert.ok(Object.hasOwn(message, 'name')) + t.assert.ok(Object.hasOwn(message, 'val')) + }) + + await t.test('has correct values', async t => { + t.assert.strictEqual(message.name, 'foo') + t.assert.strictEqual(message.val, 20) + }) + }) +}) diff --git a/recordable/plot.test.js b/recordable/plot.test.js new file mode 100644 index 0000000..26591a8 --- /dev/null +++ b/recordable/plot.test.js @@ -0,0 +1,28 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#plot()', async t => { + let plot = null + + t.beforeEach(() => { + const recordable = new Recordable() + + recordable.histogram.record(13) + recordable.histogram.record(20) + recordable.histogram.record(36) + + plot = recordable.plot() + }) + + await t.test('returns a plot', async t => { + t.assert.ok(plot) + }) + + await t.test('plot includes the min', async t => { + t.assert.ok(plot.includes('13.00'), 'plot doesnt plot min value') + }) + + await t.test('plot includes the max', async t => { + t.assert.ok(plot.includes('36.00'), 'plot doesnt plot max value') + }) +}) diff --git a/recordable/record-delta.test.js b/recordable/record-delta.test.js new file mode 100644 index 0000000..4101383 --- /dev/null +++ b/recordable/record-delta.test.js @@ -0,0 +1,83 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +const sleep = (ms = 5) => new Promise(resolve => setTimeout(resolve, ms)) + +await test('#recordDelta()', async t => { + let recordable = null + + t.beforeEach(async () => { + recordable = new Recordable() + }) + + await t.test('When the key', async t => { + await t.test('is set for the first time', async t => { + await t.test('returns 0', t => { + const result = recordable.recordDelta('baz123') + + t.assert.strictEqual(result, 0) + }) + }) + + await t.test('matches with open key', async t => { + await t.test('returns the delta', async t => { + recordable.recordDelta('baz456') + + await sleep(5) + + const result = recordable.recordDelta('baz456') + + t.assert.ok(result > 0) + t.assert.ok(result < 10) + }) + }) + }) + + + await t.test('When key is not passed', async t => { + t.beforeEach(async () => { + recordable.recordDelta('bar') + await sleep(25) + recordable.recordDelta('foo') + await sleep(15) + recordable.recordDelta('foo') + await sleep(5) + recordable.recordDelta('foo') + await sleep(10) + recordable.recordDelta('bar') + }) + + await t.test('matches with previous entries with no key', async t => { + await t.test('recorded 3 values', t => { + t.assert.strictEqual(recordable.values.length, 3) + }) + + await t.test('1st and last values are about ok', t => { + t.assert.ok(recordable.values.at(0) - 15 < 5) // tolerance + t.assert.ok(recordable.values.at(-1) - 55 < 10) // tolerance + }) + }) + }) + + await t.test('When a key is provided', async t => { + t.beforeEach(async () => { + recordable.recordDelta('bar') + recordable.recordDelta('foo') + await sleep(10) + recordable.recordDelta('foo') + await sleep(10) + recordable.recordDelta('foo') + await sleep(5) + recordable.recordDelta('bar') + }) + + await t.test('recorded 4 values', t => { + t.assert.strictEqual(recordable.values.length, 3) + }) + + await t.test('1st and last values are about ok', t => { + t.assert.ok(recordable.values.at(0) - 15 < 5) // tolerance + t.assert.ok(recordable.values.at(-1) - 50 < 10) // tolerance + }) + }) +}) diff --git a/recordable/record.test.js b/recordable/record.test.js new file mode 100644 index 0000000..4f65d80 --- /dev/null +++ b/recordable/record.test.js @@ -0,0 +1,50 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#record(val)', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable() + }) + + await t.test('"value" parameter', async t => { + await t.test('is missing', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.record() + }, { name: 'TypeError' }) + }) + + await t.test('is a float', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.record(0.01) + }, { name: 'RangeError' }) + }) + }) + + await t.test('is a negative integer', async t => { + await t.test('throws an error', t => { + t.assert.throws(() => { + recordable.record(0.01) + }, { name: 'RangeError' }) + }) + }) + }) + + await t.test('is present & valid', async t => { + t.beforeEach(() => { + recordable.record(10) + }) + + await t.test('records its own value', t => { + t.assert.strictEqual(recordable.histogram.count, 1) + }) + + await t.test('in its histogram', t => { + t.assert.strictEqual(recordable.histogram.count, 1) + }) + }) + }) +}) diff --git a/recordable/remote-patch.test.js b/recordable/remote-patch.test.js new file mode 100644 index 0000000..9c21e12 --- /dev/null +++ b/recordable/remote-patch.test.js @@ -0,0 +1,63 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#applyRemotePatch', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('name is different', async t => { + await t.test('throws Error', t => { + t.assert.throws(() => { + recordable.applyRemotePatch({ name: 'bar', val: 10 }) + }, { + name: 'RangeError' + }) + }) + }) + + await t.test('value is not a positive integer', async t => { + await t.test('throws Error', t => { + t.assert.throws(() => { + recordable.applyRemotePatch({ name: 'foo', val: 0 }) + }, { + name: 'RangeError' + }) + }) + }) + + await t.test('patch is valid', async t => { + t.beforeEach(() => { + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + recordable.applyRemotePatch({ name: 'foo', val: 20 }) + }) + + await t.test('does not throw error', t => { + t.assert.doesNotThrow(() => { + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + }) + }) + + await t.test('does not emit "value:recorded" event', async t => { + recordable.ee.on('value:recorded', e => { + }) + + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + await new Promise(resolve => setTimeout(resolve, 25)) + }) + + await t.test('increases count', t => { + t.assert.strictEqual(recordable.count, 2) + }) + + await t.test('increases mean', t => { + t.assert.strictEqual(recordable.mean, 15) + }) + + await t.test('adds to items', t => { + t.assert.strictEqual(recordable.values.length, 2) + }) + }) +}) diff --git a/recordable/reset.test.js b/recordable/reset.test.js new file mode 100644 index 0000000..38da463 --- /dev/null +++ b/recordable/reset.test.js @@ -0,0 +1,36 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#reset()', async t => { + let recordable = null + + t.beforeEach(() => { + recordable = new Recordable() + + recordable.histogram.record(10) + recordable.histogram.record(20) + recordable.histogram.record(30) + }) + + await t.test('histogram has recorded values', async t => { + await t.test('histogram has recorded values', t => { + t.assert.strictEqual(recordable.histogram.count, 3) + t.assert.ok(recordable.histogram.mean > 0, 'mean is not > 0') + }) + + await t.test('calling the .reset() method', async t => { + t.beforeEach(() => { + recordable.reset() + }) + + await t.test('resets the recorded values', t => { + t.assert.strictEqual(recordable.histogram.count, 0) + t.assert.strictEqual(recordable.histogram.mean, NaN) + }) + + await t.test('empties the accumulated values', t => { + t.assert.strictEqual(recordable.values.length, 0) + }) + }) + }) +}) diff --git a/recordable/tick.test.js b/recordable/tick.test.js new file mode 100644 index 0000000..b9d22df --- /dev/null +++ b/recordable/tick.test.js @@ -0,0 +1,13 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#tick()', async t => { + const recordable = new Recordable() + + await t.test('records a value of 1', async t => { + recordable.tick() + recordable.tick() + + t.assert.strictEqual(recordable.histogram.count, 2) + }) +}) diff --git a/recordable/to-json.test.js b/recordable/to-json.test.js new file mode 100644 index 0000000..1c0ef79 --- /dev/null +++ b/recordable/to-json.test.js @@ -0,0 +1,60 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#record(val)', async t => { + await t.test('reimporting its JSON revives it to same state', async t => { + let recordable = null + + t.beforeEach(() => { + const instance = new Recordable({ name: 'foo' }) + + for (let i = 1; i < 101; i++) + instance.record(i) + + recordable = new Recordable(JSON.parse(JSON.stringify(instance))) + }) + + await t.test('has same name', async t => { + t.assert.strictEqual(recordable.name, 'foo') + }) + + await t.test('has same values', async t => { + await t.test('has same count of values', async t => { + t.assert.strictEqual(recordable.values.length, 100) + }) + + await t.test('has same 1st value', async t => { + t.assert.strictEqual(recordable.values.at(0), 1) + }) + + await t.test('has same middle value', async t => { + t.assert.strictEqual(recordable.values.at(49), 50) + }) + + await t.test('has same last value', async t => { + t.assert.strictEqual(recordable.values.at(-1), 100) + }) + }) + + await t.test('histogram', async t => { + await t.test('is a RecordableHistogram', async t => { + const type = recordable.histogram.constructor.name + t.assert.strictEqual(type, 'RecordableHistogram') + }) + + await t.test('has same count', async t => { + t.assert.strictEqual(recordable.histogram.count, 100) + }) + + await t.test('has same means', async t => { + t.assert.strictEqual(recordable.histogram.mean, 50.5) + }) + + await t.test('can record()', async t => { + t.assert.doesNotThrow(() => { + recordable.histogram.record(1) + }) + }) + }) + }) +}) diff --git a/src/recordable-group.js b/src/recordable-group.js index 4e79afa..88a3651 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -1,4 +1,6 @@ +import { EventEmitter } from 'node:events' import { Recordable } from './recordable.js' + import asciichart from 'asciichart' import colorlist from './colorlist.js' @@ -7,8 +9,10 @@ const round = num => Math.round((num + Number.EPSILON) * 100) / 100 class RecordableGroup { constructor(...args) { + this.ee = new EventEmitter() + Object.assign(this, args.reduce((acc, name) => { - return { ...acc, [name]: new Recordable({ name }) } + return { ...acc, [name]: this.createNewRecordable(name) } }, {})) } @@ -58,17 +62,16 @@ class RecordableGroup { const width = (process.stdout.columns || 100) - 40 const height = (process.stdout.rows || 30) - 15 const values = this.getMembers() - .map((m, i) => ({ - ...m, - averages: m.toClampedAverages(width), + .map((member, i) => ({ + ...member, + averages: member.toClampedAverages(width), color: colors[i % colors.length], - name: m.name - })).filter(m => m.averages.length) + name: member.name + })).filter(member => member.averages.length) - - const plot = values.map(m => m.averages).length - ? asciichart.plot(values.map(m => m.averages), { - height, colors: values.map(m => m.color) + const plot = values.map(member => member.averages).length + ? asciichart.plot(values.map(member => member.averages), { + height, colors: values.map(member => member.color) }) : null console.clear() @@ -76,15 +79,36 @@ class RecordableGroup { if (!values.length) return console.info('not enough data to plot yet ...') - plot - ? console.log('\n'.repeat(5), plot) - : console.info('\n'.repeat(5), 'not enough plot data yet ...') + plot ? console.log('\n'.repeat(5), plot) + : console.info('\n'.repeat(5), 'not enough plot data yet ...') return plot } + createNewRecordable(name) { + const instance = new Recordable({ name }) + + instance.ee.on('value:recorded', this.emitPatchEvent.bind(this)) + + return instance + } + + applyRemotePatch({ name, val }) { + const member = this[name] + + if (member) + member.applyRemotePatch({ name, val }) + + return !!member + } + + emitPatchEvent({ name, val }) { + this.ee.emit('value:recorded', { name, val }) + } + getMembers() { - return Object.values(this).filter(value => value instanceof Recordable) + return Object.values(this) + .filter(value => value instanceof Recordable) } } diff --git a/src/recordable.js b/src/recordable.js index 3913bc6..2ef3b47 100644 --- a/src/recordable.js +++ b/src/recordable.js @@ -1,4 +1,5 @@ import { createHistogram } from 'node:perf_hooks' +import { EventEmitter } from 'node:events' import asciichart from 'asciichart' class Recordable { @@ -7,6 +8,7 @@ class Recordable { this.values = values this.deltaKeys = {} this.percentiles = {} + this.ee = new EventEmitter() this.histogram = values.length ? this.#createHistogramFromValues(values) : createHistogram() @@ -41,11 +43,14 @@ class Recordable { }) }) - this._recordFn = this.histogram.record.bind(this.histogram) + this.histogramRecord = this.histogram.record.bind(this.histogram) this.histogram.record = val => { - const result = this._recordFn(val) + const result = this.histogramRecord(val) this.values.push(val) + this.ee.emit('value:recorded', { + name: this.name, val + }) return result } @@ -125,6 +130,14 @@ class Recordable { }, []) } + applyRemotePatch({ name, val }) { + if (name !== this.name) + throw RangeError('name mismatch') + + this.histogramRecord(val) + this.values.push(val) + } + #createHistogramFromValues(values) { return values.reduce((histogram, value) => { histogram.record(value) diff --git a/test/index.test.js b/test/index.test.js index 647f4f0..f621832 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1 +1,2 @@ import './recordable/index.test.js' +import './recordable-group/index.test.js' diff --git a/test/recordable-group/index.test.js b/test/recordable-group/index.test.js new file mode 100644 index 0000000..d651272 --- /dev/null +++ b/test/recordable-group/index.test.js @@ -0,0 +1,2 @@ +import './patch-event.test.js' +import './remote-patch.test.js' diff --git a/test/recordable-group/patch-event.test.js b/test/recordable-group/patch-event.test.js new file mode 100644 index 0000000..31581cc --- /dev/null +++ b/test/recordable-group/patch-event.test.js @@ -0,0 +1,32 @@ +import test from 'node:test' +import { Recordable, RecordableGroup } from '../../index.js' + +await test('#patchEvent - group', async t => { + let group, message = null + + t.beforeEach(async () => { + group = new RecordableGroup('foo', 'bar') + group.ee.on('value:recorded', msg => { + message = msg + }) + + group.foo.record(20) + + return new Promise(res => setTimeout(res, 50)) + }) + + await t.test('event is fired', async t => { + t.assert.ok(message) + }) + + await t.test('message look ok', async t => { + t.assert.ok(typeof message, 'object') + t.assert.ok(Object.hasOwn(message, 'name')) + t.assert.ok(Object.hasOwn(message, 'val')) + }) + + await t.test('has correct values', async t => { + t.assert.strictEqual(message.name, 'foo') + t.assert.strictEqual(message.val, 20) + }) +}) diff --git a/test/recordable-group/remote-patch.test.js b/test/recordable-group/remote-patch.test.js new file mode 100644 index 0000000..57a2e2b --- /dev/null +++ b/test/recordable-group/remote-patch.test.js @@ -0,0 +1,84 @@ +import test from 'node:test' +import { Recordable, RecordableGroup } from '../../index.js' + +await test('#applyRemotePatch', async t => { + let group, recordable + + t.beforeEach(() => { + recordable = new Recordable() + group = new RecordableGroup('foo', 'bar') + }) + + await t.test('nobody in group has name of patch', async t => { + group.applyRemotePatch({ name: 'baz', val: 10 }) + await t.test('count stays same', t => { + t.assert.strictEqual(group.foo.count, 0) + t.assert.strictEqual(group.bar.count, 0) + }) + }) + + await t.test('one member has the name of patch', async t => { + t.beforeEach(() => { + for (let i = 0; i < 20; i++) + group.applyRemotePatch({ name: 'foo', val: 10 }) + }) + + await t.test('it is applied to him', async t => { + await t.test('does not emit "value:recorded" event', async t => { + recordable.ee.on('value:recorded', e => { + throw new Error("erroneously called") + }) + + group.applyRemotePatch({ name: 'foo', val: 10 }) + await new Promise(resolve => setTimeout(resolve, 25)) + }) + + await t.test('increases count', t => { + t.assert.strictEqual(group.foo.count, 20) + t.assert.strictEqual(group.bar.count, 0) + }) + + await t.test('increases mean', t => { + t.assert.strictEqual(group.foo.mean, 10) + }) + + await t.test('adds to items', t => { + t.assert.strictEqual(group.foo.values.length, 20) + }) + }) + }) + + await t.test('patch is valid', async t => { + t.beforeEach(() => { + group.applyRemotePatch({ name: 'foo', val: 10 }) + group.applyRemotePatch({ name: 'foo', val: 20 }) + }) + + await t.test('does not throw error', t => { + t.assert.doesNotThrow(() => { + group.applyRemotePatch({ name: 'foo', val: 10 }) + }) + }) + + await t.test('does not emit "value:recorded" event', async t => { + group.foo.ee.on('value:recorded', e => { + throw new Error("erroneously called") + }) + + group.applyRemotePatch({ name: 'foo', val: 10 }) + await new Promise(resolve => setTimeout(resolve, 25)) + }) + + await t.test('increases count', t => { + t.assert.strictEqual(group.foo.count, 2) + }) + + await t.test('increases mean', t => { + t.assert.strictEqual(group.foo.mean, 15) + }) + + await t.test('adds to items', t => { + t.assert.strictEqual(group.foo.values.length, 2) + }) + }) +}) diff --git a/test/recordable/patch-event.test.js b/test/recordable/patch-event.test.js new file mode 100644 index 0000000..0b6d90e --- /dev/null +++ b/test/recordable/patch-event.test.js @@ -0,0 +1,39 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#patchEvent', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('a value is recorded', async t => { + let message = null + + t.beforeEach(async () => { + recordable.ee.on('value:recorded', msg => { + message = msg + }) + + recordable.record(20) + + return new Promise(res => setTimeout(res, 50)) + }) + + await t.test('event is fired', async t => { + t.assert.ok(message) + }) + + await t.test('message look ok', async t => { + t.assert.ok(typeof message, 'object') + t.assert.ok(Object.hasOwn(message, 'name')) + t.assert.ok(Object.hasOwn(message, 'val')) + }) + + await t.test('has correct values', async t => { + t.assert.strictEqual(message.name, 'foo') + t.assert.strictEqual(message.val, 20) + }) + }) +}) diff --git a/test/recordable/remote-patch.test.js b/test/recordable/remote-patch.test.js new file mode 100644 index 0000000..9c21e12 --- /dev/null +++ b/test/recordable/remote-patch.test.js @@ -0,0 +1,63 @@ +import test from 'node:test' +import { Recordable } from '../../index.js' + +await test('#applyRemotePatch', async t => { + let recordable + + t.beforeEach(() => { + recordable = new Recordable({ name: 'foo' }) + }) + + await t.test('name is different', async t => { + await t.test('throws Error', t => { + t.assert.throws(() => { + recordable.applyRemotePatch({ name: 'bar', val: 10 }) + }, { + name: 'RangeError' + }) + }) + }) + + await t.test('value is not a positive integer', async t => { + await t.test('throws Error', t => { + t.assert.throws(() => { + recordable.applyRemotePatch({ name: 'foo', val: 0 }) + }, { + name: 'RangeError' + }) + }) + }) + + await t.test('patch is valid', async t => { + t.beforeEach(() => { + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + recordable.applyRemotePatch({ name: 'foo', val: 20 }) + }) + + await t.test('does not throw error', t => { + t.assert.doesNotThrow(() => { + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + }) + }) + + await t.test('does not emit "value:recorded" event', async t => { + recordable.ee.on('value:recorded', e => { + }) + + recordable.applyRemotePatch({ name: 'foo', val: 10 }) + await new Promise(resolve => setTimeout(resolve, 25)) + }) + + await t.test('increases count', t => { + t.assert.strictEqual(recordable.count, 2) + }) + + await t.test('increases mean', t => { + t.assert.strictEqual(recordable.mean, 15) + }) + + await t.test('adds to items', t => { + t.assert.strictEqual(recordable.values.length, 2) + }) + }) +}) From 0cff2ab7e4bb16486eb388b88a530b6b4f19b627 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 09:59:43 +0300 Subject: [PATCH 04/19] feat: small tweaks --- src/recordable-group.js | 10 +++++++--- src/recordable.js | 5 +++++ test/recordable-group/patch-event.test.js | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/recordable-group.js b/src/recordable-group.js index 88a3651..a376fcb 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -88,7 +88,7 @@ class RecordableGroup { createNewRecordable(name) { const instance = new Recordable({ name }) - instance.ee.on('value:recorded', this.emitPatchEvent.bind(this)) + instance.on('value:recorded', this.emitPatchEvent.bind(this)) return instance } @@ -102,14 +102,18 @@ class RecordableGroup { return !!member } - emitPatchEvent({ name, val }) { - this.ee.emit('value:recorded', { name, val }) + emitPatchEvent(message) { + this.ee.emit('value:recorded', message) } getMembers() { return Object.values(this) .filter(value => value instanceof Recordable) } + + on(...args) { + return this.ee.on(...args) + } } export { RecordableGroup } diff --git a/src/recordable.js b/src/recordable.js index 2ef3b47..9d1f203 100644 --- a/src/recordable.js +++ b/src/recordable.js @@ -49,6 +49,7 @@ class Recordable { this.values.push(val) this.ee.emit('value:recorded', { + type: 'value:recorded', name: this.name, val }) @@ -138,6 +139,10 @@ class Recordable { this.values.push(val) } + on(...args) { + return this.ee.on(...args) + } + #createHistogramFromValues(values) { return values.reduce((histogram, value) => { histogram.record(value) diff --git a/test/recordable-group/patch-event.test.js b/test/recordable-group/patch-event.test.js index 31581cc..8ba47de 100644 --- a/test/recordable-group/patch-event.test.js +++ b/test/recordable-group/patch-event.test.js @@ -6,7 +6,7 @@ await test('#patchEvent - group', async t => { t.beforeEach(async () => { group = new RecordableGroup('foo', 'bar') - group.ee.on('value:recorded', msg => { + group.on('value:recorded', msg => { message = msg }) From 04b03c7fd0835d7f25f9fc4afa705d2defb1a035 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 10:05:02 +0300 Subject: [PATCH 05/19] feat: bindForUpdates protot --- src/recordable-group.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/recordable-group.js b/src/recordable-group.js index a376fcb..bd80ad6 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -111,6 +111,13 @@ class RecordableGroup { .filter(value => value instanceof Recordable) } + bindForUpdates({ type, name, val }) { + if (type !== 'value:recorded') + return + console.log(type, name, value) + return applyRemotePatch({ name, val }) + } + on(...args) { return this.ee.on(...args) } From 1c9d8dfc5b058573b800728e751c405f2b611ea6 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 10:13:37 +0300 Subject: [PATCH 06/19] feat: small tweaks --- src/recordable-group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recordable-group.js b/src/recordable-group.js index bd80ad6..760c05a 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -114,7 +114,7 @@ class RecordableGroup { bindForUpdates({ type, name, val }) { if (type !== 'value:recorded') return - console.log(type, name, value) + return applyRemotePatch({ name, val }) } From 52c103796639a83021b67bba457b8cf4f2490f10 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 10:17:56 +0300 Subject: [PATCH 07/19] feat: small tweaks --- src/recordable-group.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/recordable-group.js b/src/recordable-group.js index 760c05a..10e6885 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -93,11 +93,11 @@ class RecordableGroup { return instance } - applyRemotePatch({ name, val }) { + applyRemotePatch(patch) { const member = this[name] if (member) - member.applyRemotePatch({ name, val }) + member.applyRemotePatch(patch) return !!member } @@ -111,11 +111,11 @@ class RecordableGroup { .filter(value => value instanceof Recordable) } - bindForUpdates({ type, name, val }) { - if (type !== 'value:recorded') + bindForUpdates(patch) { + if (patch && patch.type !== 'value:recorded') return - return applyRemotePatch({ name, val }) + return this.applyRemotePatch(patch) } on(...args) { From ef777c345c584aa62ca512996ebd357282e9056b Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 10:24:47 +0300 Subject: [PATCH 08/19] feat: small tweaks --- src/recordable-group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recordable-group.js b/src/recordable-group.js index 10e6885..e3c6fb7 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -94,7 +94,7 @@ class RecordableGroup { } applyRemotePatch(patch) { - const member = this[name] + const member = this[patch.name] if (member) member.applyRemotePatch(patch) From f319df508c15fe9261af10154ca2b08154cba9c1 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 10:26:21 +0300 Subject: [PATCH 09/19] feat: small tweaks --- src/recordable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/recordable.js b/src/recordable.js index 9d1f203..f4ae4a5 100644 --- a/src/recordable.js +++ b/src/recordable.js @@ -132,6 +132,7 @@ class Recordable { } applyRemotePatch({ name, val }) { + console.log(name, this.name) if (name !== this.name) throw RangeError('name mismatch') From 3f384206a9af5460fed6efc1e654c82c031815f6 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 10:27:01 +0300 Subject: [PATCH 10/19] feat: small tweaks --- src/recordable.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/recordable.js b/src/recordable.js index f4ae4a5..d975c28 100644 --- a/src/recordable.js +++ b/src/recordable.js @@ -132,8 +132,7 @@ class Recordable { } applyRemotePatch({ name, val }) { - console.log(name, this.name) - if (name !== this.name) + if (name != this.name) throw RangeError('name mismatch') this.histogramRecord(val) From 328b273a3e2d88d0c61e9fdf39d324d55b736cd9 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 11:19:05 +0300 Subject: [PATCH 11/19] feat: remove event emitters --- src/recordable-group.js | 35 ++------- src/recordable.js | 18 ----- test/index.test.js | 1 - test/recordable-group/index.test.js | 2 - test/recordable-group/patch-event.test.js | 32 --------- test/recordable-group/remote-patch.test.js | 84 ---------------------- test/recordable/patch-event.test.js | 39 ---------- test/recordable/remote-patch.test.js | 63 ---------------- 8 files changed, 4 insertions(+), 270 deletions(-) delete mode 100644 test/recordable-group/index.test.js delete mode 100644 test/recordable-group/patch-event.test.js delete mode 100644 test/recordable-group/remote-patch.test.js delete mode 100644 test/recordable/patch-event.test.js delete mode 100644 test/recordable/remote-patch.test.js diff --git a/src/recordable-group.js b/src/recordable-group.js index e3c6fb7..07e2027 100644 --- a/src/recordable-group.js +++ b/src/recordable-group.js @@ -1,4 +1,3 @@ -import { EventEmitter } from 'node:events' import { Recordable } from './recordable.js' import asciichart from 'asciichart' @@ -9,10 +8,8 @@ const round = num => Math.round((num + Number.EPSILON) * 100) / 100 class RecordableGroup { constructor(...args) { - this.ee = new EventEmitter() - Object.assign(this, args.reduce((acc, name) => { - return { ...acc, [name]: this.createNewRecordable(name) } + return { ...acc, [name]: new Recordable({ name }) } }, {})) } @@ -85,41 +82,17 @@ class RecordableGroup { return plot } - createNewRecordable(name) { - const instance = new Recordable({ name }) - - instance.on('value:recorded', this.emitPatchEvent.bind(this)) - - return instance - } - - applyRemotePatch(patch) { - const member = this[patch.name] - - if (member) - member.applyRemotePatch(patch) - - return !!member - } - - emitPatchEvent(message) { - this.ee.emit('value:recorded', message) - } - getMembers() { return Object.values(this) .filter(value => value instanceof Recordable) } - bindForUpdates(patch) { - if (patch && patch.type !== 'value:recorded') - return + getRow() { - return this.applyRemotePatch(patch) } - on(...args) { - return this.ee.on(...args) + applyRow() { + } } diff --git a/src/recordable.js b/src/recordable.js index d975c28..1f58193 100644 --- a/src/recordable.js +++ b/src/recordable.js @@ -1,5 +1,4 @@ import { createHistogram } from 'node:perf_hooks' -import { EventEmitter } from 'node:events' import asciichart from 'asciichart' class Recordable { @@ -8,7 +7,6 @@ class Recordable { this.values = values this.deltaKeys = {} this.percentiles = {} - this.ee = new EventEmitter() this.histogram = values.length ? this.#createHistogramFromValues(values) : createHistogram() @@ -48,10 +46,6 @@ class Recordable { const result = this.histogramRecord(val) this.values.push(val) - this.ee.emit('value:recorded', { - type: 'value:recorded', - name: this.name, val - }) return result } @@ -131,18 +125,6 @@ class Recordable { }, []) } - applyRemotePatch({ name, val }) { - if (name != this.name) - throw RangeError('name mismatch') - - this.histogramRecord(val) - this.values.push(val) - } - - on(...args) { - return this.ee.on(...args) - } - #createHistogramFromValues(values) { return values.reduce((histogram, value) => { histogram.record(value) diff --git a/test/index.test.js b/test/index.test.js index f621832..647f4f0 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,2 +1 @@ import './recordable/index.test.js' -import './recordable-group/index.test.js' diff --git a/test/recordable-group/index.test.js b/test/recordable-group/index.test.js deleted file mode 100644 index d651272..0000000 --- a/test/recordable-group/index.test.js +++ /dev/null @@ -1,2 +0,0 @@ -import './patch-event.test.js' -import './remote-patch.test.js' diff --git a/test/recordable-group/patch-event.test.js b/test/recordable-group/patch-event.test.js deleted file mode 100644 index 8ba47de..0000000 --- a/test/recordable-group/patch-event.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import test from 'node:test' -import { Recordable, RecordableGroup } from '../../index.js' - -await test('#patchEvent - group', async t => { - let group, message = null - - t.beforeEach(async () => { - group = new RecordableGroup('foo', 'bar') - group.on('value:recorded', msg => { - message = msg - }) - - group.foo.record(20) - - return new Promise(res => setTimeout(res, 50)) - }) - - await t.test('event is fired', async t => { - t.assert.ok(message) - }) - - await t.test('message look ok', async t => { - t.assert.ok(typeof message, 'object') - t.assert.ok(Object.hasOwn(message, 'name')) - t.assert.ok(Object.hasOwn(message, 'val')) - }) - - await t.test('has correct values', async t => { - t.assert.strictEqual(message.name, 'foo') - t.assert.strictEqual(message.val, 20) - }) -}) diff --git a/test/recordable-group/remote-patch.test.js b/test/recordable-group/remote-patch.test.js deleted file mode 100644 index 57a2e2b..0000000 --- a/test/recordable-group/remote-patch.test.js +++ /dev/null @@ -1,84 +0,0 @@ -import test from 'node:test' -import { Recordable, RecordableGroup } from '../../index.js' - -await test('#applyRemotePatch', async t => { - let group, recordable - - t.beforeEach(() => { - recordable = new Recordable() - group = new RecordableGroup('foo', 'bar') - }) - - await t.test('nobody in group has name of patch', async t => { - group.applyRemotePatch({ name: 'baz', val: 10 }) - await t.test('count stays same', t => { - t.assert.strictEqual(group.foo.count, 0) - t.assert.strictEqual(group.bar.count, 0) - }) - }) - - await t.test('one member has the name of patch', async t => { - t.beforeEach(() => { - for (let i = 0; i < 20; i++) - group.applyRemotePatch({ name: 'foo', val: 10 }) - }) - - await t.test('it is applied to him', async t => { - await t.test('does not emit "value:recorded" event', async t => { - recordable.ee.on('value:recorded', e => { - throw new Error("erroneously called") - }) - - group.applyRemotePatch({ name: 'foo', val: 10 }) - await new Promise(resolve => setTimeout(resolve, 25)) - }) - - await t.test('increases count', t => { - t.assert.strictEqual(group.foo.count, 20) - t.assert.strictEqual(group.bar.count, 0) - }) - - await t.test('increases mean', t => { - t.assert.strictEqual(group.foo.mean, 10) - }) - - await t.test('adds to items', t => { - t.assert.strictEqual(group.foo.values.length, 20) - }) - }) - }) - - await t.test('patch is valid', async t => { - t.beforeEach(() => { - group.applyRemotePatch({ name: 'foo', val: 10 }) - group.applyRemotePatch({ name: 'foo', val: 20 }) - }) - - await t.test('does not throw error', t => { - t.assert.doesNotThrow(() => { - group.applyRemotePatch({ name: 'foo', val: 10 }) - }) - }) - - await t.test('does not emit "value:recorded" event', async t => { - group.foo.ee.on('value:recorded', e => { - throw new Error("erroneously called") - }) - - group.applyRemotePatch({ name: 'foo', val: 10 }) - await new Promise(resolve => setTimeout(resolve, 25)) - }) - - await t.test('increases count', t => { - t.assert.strictEqual(group.foo.count, 2) - }) - - await t.test('increases mean', t => { - t.assert.strictEqual(group.foo.mean, 15) - }) - - await t.test('adds to items', t => { - t.assert.strictEqual(group.foo.values.length, 2) - }) - }) -}) diff --git a/test/recordable/patch-event.test.js b/test/recordable/patch-event.test.js deleted file mode 100644 index 0b6d90e..0000000 --- a/test/recordable/patch-event.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import test from 'node:test' -import { Recordable } from '../../index.js' - -await test('#patchEvent', async t => { - let recordable - - t.beforeEach(() => { - recordable = new Recordable({ name: 'foo' }) - }) - - await t.test('a value is recorded', async t => { - let message = null - - t.beforeEach(async () => { - recordable.ee.on('value:recorded', msg => { - message = msg - }) - - recordable.record(20) - - return new Promise(res => setTimeout(res, 50)) - }) - - await t.test('event is fired', async t => { - t.assert.ok(message) - }) - - await t.test('message look ok', async t => { - t.assert.ok(typeof message, 'object') - t.assert.ok(Object.hasOwn(message, 'name')) - t.assert.ok(Object.hasOwn(message, 'val')) - }) - - await t.test('has correct values', async t => { - t.assert.strictEqual(message.name, 'foo') - t.assert.strictEqual(message.val, 20) - }) - }) -}) diff --git a/test/recordable/remote-patch.test.js b/test/recordable/remote-patch.test.js deleted file mode 100644 index 9c21e12..0000000 --- a/test/recordable/remote-patch.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import test from 'node:test' -import { Recordable } from '../../index.js' - -await test('#applyRemotePatch', async t => { - let recordable - - t.beforeEach(() => { - recordable = new Recordable({ name: 'foo' }) - }) - - await t.test('name is different', async t => { - await t.test('throws Error', t => { - t.assert.throws(() => { - recordable.applyRemotePatch({ name: 'bar', val: 10 }) - }, { - name: 'RangeError' - }) - }) - }) - - await t.test('value is not a positive integer', async t => { - await t.test('throws Error', t => { - t.assert.throws(() => { - recordable.applyRemotePatch({ name: 'foo', val: 0 }) - }, { - name: 'RangeError' - }) - }) - }) - - await t.test('patch is valid', async t => { - t.beforeEach(() => { - recordable.applyRemotePatch({ name: 'foo', val: 10 }) - recordable.applyRemotePatch({ name: 'foo', val: 20 }) - }) - - await t.test('does not throw error', t => { - t.assert.doesNotThrow(() => { - recordable.applyRemotePatch({ name: 'foo', val: 10 }) - }) - }) - - await t.test('does not emit "value:recorded" event', async t => { - recordable.ee.on('value:recorded', e => { - }) - - recordable.applyRemotePatch({ name: 'foo', val: 10 }) - await new Promise(resolve => setTimeout(resolve, 25)) - }) - - await t.test('increases count', t => { - t.assert.strictEqual(recordable.count, 2) - }) - - await t.test('increases mean', t => { - t.assert.strictEqual(recordable.mean, 15) - }) - - await t.test('adds to items', t => { - t.assert.strictEqual(recordable.values.length, 2) - }) - }) -}) From cf39b935a749d8dc406494545fd47f148458b9a1 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 12:16:45 +0300 Subject: [PATCH 12/19] refactor: use the row concept --- index.js | 5 ++- src/recordable-group.js | 99 ----------------------------------------- src/recordable-row.js | 29 ++++++++++++ src/row-viewer.js | 52 ++++++++++++++++++++++ 4 files changed, 84 insertions(+), 101 deletions(-) delete mode 100644 src/recordable-group.js create mode 100644 src/recordable-row.js create mode 100644 src/row-viewer.js diff --git a/index.js b/index.js index cbf91a8..db0514b 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ import { Recordable } from './src/recordable.js' -import { RecordableGroup } from './src/recordable-group.js' +import { RecordableRow } from './src/recordable-row.js' +import RowViewer from './src/row-viewer.js' -export { Recordable, RecordableGroup } +export { Recordable, RecordableRow, RowViewer } diff --git a/src/recordable-group.js b/src/recordable-group.js deleted file mode 100644 index 07e2027..0000000 --- a/src/recordable-group.js +++ /dev/null @@ -1,99 +0,0 @@ -import { Recordable } from './recordable.js' - -import asciichart from 'asciichart' -import colorlist from './colorlist.js' - -const colors = Object.values(colorlist) -const round = num => Math.round((num + Number.EPSILON) * 100) / 100 - -class RecordableGroup { - constructor(...args) { - Object.assign(this, args.reduce((acc, name) => { - return { ...acc, [name]: new Recordable({ name }) } - }, {})) - } - - histograms({ maxRows, sortBy, title = 'Untitled' }) { - return this.print({ - ...{ maxRows, sortBy }, - transformFn: ({ key, value }) => { - const histogram = value.histogram.toJSON() - - return { - 'count' : histogram.count, - 'min (ms)' : round(histogram.min), - 'mean (ms)' : round(histogram.mean), - 'max (ms)' : round(histogram.max), - 'dev (ms)' : round(histogram.stddev), - } - } - }) - } - - print({ - title = '', - maxRows = Infinity, - sortBy = 'pid', - transformFn = () => {} - } = {}) { - const values = Object.entries(this) - .filter(entry => entry[1] instanceof Recordable) - .sort((a, b) => b[sortBy] - a[sortBy]) - .slice(0, maxRows) - .map(([key, value]) => transformFn({ key, value })) - - console.clear() - console.log('\n', '\n') - console.log('Title:', title) - console.table(values) - - const hidden = this.getMembers().length - maxRows - - if (hidden > 1) - console.log('... plus:', hidden, 'hidden') - - console.log('\n') - } - - plot() { - const width = (process.stdout.columns || 100) - 40 - const height = (process.stdout.rows || 30) - 15 - const values = this.getMembers() - .map((member, i) => ({ - ...member, - averages: member.toClampedAverages(width), - color: colors[i % colors.length], - name: member.name - })).filter(member => member.averages.length) - - const plot = values.map(member => member.averages).length - ? asciichart.plot(values.map(member => member.averages), { - height, colors: values.map(member => member.color) - }) : null - - console.clear() - - if (!values.length) - return console.info('not enough data to plot yet ...') - - plot ? console.log('\n'.repeat(5), plot) - : console.info('\n'.repeat(5), 'not enough plot data yet ...') - - return plot - } - - getMembers() { - return Object.values(this) - .filter(value => value instanceof Recordable) - } - - getRow() { - - } - - applyRow() { - - } -} - -export { RecordableGroup } diff --git a/src/recordable-row.js b/src/recordable-row.js new file mode 100644 index 0000000..0cc0865 --- /dev/null +++ b/src/recordable-row.js @@ -0,0 +1,29 @@ +import { Recordable } from './recordable.js' + +class RecordableRow { + constructor(...args) { + Object.assign(this, args.reduce((acc, name) => { + return { ...acc, [name]: new Recordable({ name }) } + }, {})) + } + + getMembers() { + return Object.values(this) + .filter(value => value instanceof Recordable) + } + + getRow() { + return this.getMembers().reduce((acc, member) => { + acc.members[member.name] = { + histogram: member.histogram.toJSON() + } + + return acc + }, { + id: process.pid, + members: [] + }) + } +} + +export { RecordableRow } diff --git a/src/row-viewer.js b/src/row-viewer.js new file mode 100644 index 0000000..1e58051 --- /dev/null +++ b/src/row-viewer.js @@ -0,0 +1,52 @@ +class RowViewer { + constructor({ + title = '', + subtitle = '', + maxRows = 5, + fields = [ + ['foo.mean', 'Mean Task Duration (ms)'], + ['bar.count', 'Total Count'] + ], + sort = function(a, b) { a - b }, + updateInterval = 500 + } = {}) { + this.rows = [] + this.timer = setInterval(() => { + const values = this.rows + .sort(sort) + .slice(this.rows.length - maxRows, this.rows.length) + .map(row => { + return fields.reduce((acc, field) => { + const split = field[0].split('.') + return { + ...acc, + [field[1]] : row.members[split[0]].histogram[split[1]] + } + }, {}) + }) + + console.clear() + console.log('\n', '\n') + if (title) + console.log('Title:', title) + + console.table(values) + + if (subtitle) + console.log('Title:', title) + + if (subtitle) + console.log(subtitle) + }, updateInterval) + } + + append(row) { + this.rows.push(row) + } + + stop() { + clearInterval(this.timer) + } +} + +export default RowViewer From 462f7c6c9f3079ef8b09671d5a8e3e88746c33d1 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 12:36:14 +0300 Subject: [PATCH 13/19] refactor: use the row concept --- src/recordable-row.js | 12 ++++-------- src/row-viewer.js | 3 ++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/recordable-row.js b/src/recordable-row.js index 0cc0865..9e3a851 100644 --- a/src/recordable-row.js +++ b/src/recordable-row.js @@ -14,15 +14,11 @@ class RecordableRow { getRow() { return this.getMembers().reduce((acc, member) => { - acc.members[member.name] = { - histogram: member.histogram.toJSON() + return { + ...acc, + [member.name]: member.histogram.toJSON() } - - return acc - }, { - id: process.pid, - members: [] - }) + }, { id: process.pid }) } } diff --git a/src/row-viewer.js b/src/row-viewer.js index 1e58051..064437f 100644 --- a/src/row-viewer.js +++ b/src/row-viewer.js @@ -20,13 +20,14 @@ class RowViewer { const split = field[0].split('.') return { ...acc, - [field[1]] : row.members[split[0]].histogram[split[1]] + [field[1]] : row[split[0]][split[1]] } }, {}) }) console.clear() console.log('\n', '\n') + if (title) console.log('Title:', title) From 9d3b1a0ce47b4d73d9fd2a07efc318dd93041c08 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 12:44:02 +0300 Subject: [PATCH 14/19] refactor: use the row concept --- src/row-viewer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/row-viewer.js b/src/row-viewer.js index 064437f..19eaff8 100644 --- a/src/row-viewer.js +++ b/src/row-viewer.js @@ -20,7 +20,9 @@ class RowViewer { const split = field[0].split('.') return { ...acc, - [field[1]] : row[split[0]][split[1]] + [field[1]] : row[2] + ? row[2](row[split[0]][split[1]]) + : row[split[0]][split[1]] } }, {}) }) @@ -38,6 +40,8 @@ class RowViewer { if (subtitle) console.log(subtitle) + + console.log('\n', '\n') }, updateInterval) } From f69cad04d230f753ca058515ba3917abf081aab3 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 14:21:44 +0300 Subject: [PATCH 15/19] refactor: use the row concept --- src/recordable-row.js | 6 +-- src/row-viewer.js | 86 +++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/src/recordable-row.js b/src/recordable-row.js index 9e3a851..87163c0 100644 --- a/src/recordable-row.js +++ b/src/recordable-row.js @@ -8,15 +8,13 @@ class RecordableRow { } getMembers() { - return Object.values(this) - .filter(value => value instanceof Recordable) + return Object.values(this).filter(value => value instanceof Recordable) } getRow() { return this.getMembers().reduce((acc, member) => { return { - ...acc, - [member.name]: member.histogram.toJSON() + ...acc, [member.name]: member.histogram.toJSON() } }, { id: process.pid }) } diff --git a/src/row-viewer.js b/src/row-viewer.js index 19eaff8..e6d0a53 100644 --- a/src/row-viewer.js +++ b/src/row-viewer.js @@ -1,48 +1,56 @@ class RowViewer { constructor({ - title = '', - subtitle = '', - maxRows = 5, + title = '', subtitle = '', maxRows = 5, updateInterval = 500, + sort = function(a, b) { a - b }, fields = [ ['foo.mean', 'Mean Task Duration (ms)'], ['bar.count', 'Total Count'] - ], - sort = function(a, b) { a - b }, - updateInterval = 500 - } = {}) { + ] + } = {}, recordableRow = null) { + this.title = title + this.subtitle = subtitle + this.maxRows = maxRows + this.fields = fields + this.rows = [] - this.timer = setInterval(() => { - const values = this.rows - .sort(sort) - .slice(this.rows.length - maxRows, this.rows.length) - .map(row => { - return fields.reduce((acc, field) => { - const split = field[0].split('.') - return { - ...acc, - [field[1]] : row[2] - ? row[2](row[split[0]][split[1]]) - : row[split[0]][split[1]] - } - }, {}) - }) - - console.clear() - console.log('\n', '\n') - - if (title) - console.log('Title:', title) - - console.table(values) - - if (subtitle) - console.log('Title:', title) - - if (subtitle) - console.log(subtitle) - - console.log('\n', '\n') - }, updateInterval) + this.recordableRow = recordableRow + this.sort = sort + this.timer = setInterval(this.render.bind(this), updateInterval) + } + + render() { + if (this.recordableRow) + this.append(this.recordableRow.getRow()) + + const values = this.rows + .sort(this.sort) + .slice(this.rows.length - this.maxRows, this.rows.length) + .map(row => { + return this.fields.reduce((acc, field) => { + const split = field[0].split('.') + return { + ...acc, + [field[1]] : row[2] + ? row[2](row[split[0]][split[1]]) + : row[split[0]][split[1]] + } + }, {}) + }) + + console.clear() + console.log('\n', '\n') + + this.title + ? console.log('Title:', this.title) + : null + + console.table(values) + + this.subtitle + ? console.log(this.subtitle) + : null + + console.log('\n', '\n') } append(row) { From 97597c298a7da6667c616e500a5898132ff17c2b Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 14:46:30 +0300 Subject: [PATCH 16/19] refactor: use the row concept --- index.js | 7 +++-- src/{row-viewer.js => stats-list.js} | 22 +++----------- src/{recordable-row.js => stats-store.js} | 4 +-- src/stats-view.js | 36 +++++++++++++++++++++++ 4 files changed, 46 insertions(+), 23 deletions(-) rename src/{row-viewer.js => stats-list.js} (68%) rename src/{recordable-row.js => stats-store.js} (91%) create mode 100644 src/stats-view.js diff --git a/index.js b/index.js index db0514b..ea3e354 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ +import { StatsView } from './src/stats-view.js' +import { StatsList } from './src/stats-list.js' +import { StatsStore } from './src/stats-store.js' import { Recordable } from './src/recordable.js' -import { RecordableRow } from './src/recordable-row.js' -import RowViewer from './src/row-viewer.js' -export { Recordable, RecordableRow, RowViewer } +export { Recordable, StatsStore, StatsList, StatsView } diff --git a/src/row-viewer.js b/src/stats-list.js similarity index 68% rename from src/row-viewer.js rename to src/stats-list.js index e6d0a53..11e09fc 100644 --- a/src/row-viewer.js +++ b/src/stats-list.js @@ -1,27 +1,22 @@ -class RowViewer { +class StatsList { constructor({ - title = '', subtitle = '', maxRows = 5, updateInterval = 500, + title = '', subtitle = '', maxRows = 5, sort = function(a, b) { a - b }, fields = [ ['foo.mean', 'Mean Task Duration (ms)'], ['bar.count', 'Total Count'] ] - } = {}, recordableRow = null) { + } = {}) { this.title = title this.subtitle = subtitle this.maxRows = maxRows this.fields = fields this.rows = [] - this.recordableRow = recordableRow this.sort = sort - this.timer = setInterval(this.render.bind(this), updateInterval) } render() { - if (this.recordableRow) - this.append(this.recordableRow.getRow()) - const values = this.rows .sort(this.sort) .slice(this.rows.length - this.maxRows, this.rows.length) @@ -37,9 +32,6 @@ class RowViewer { }, {}) }) - console.clear() - console.log('\n', '\n') - this.title ? console.log('Title:', this.title) : null @@ -49,17 +41,11 @@ class RowViewer { this.subtitle ? console.log(this.subtitle) : null - - console.log('\n', '\n') } append(row) { this.rows.push(row) } - - stop() { - clearInterval(this.timer) - } } -export default RowViewer +export { StatsList } diff --git a/src/recordable-row.js b/src/stats-store.js similarity index 91% rename from src/recordable-row.js rename to src/stats-store.js index 87163c0..5358e29 100644 --- a/src/recordable-row.js +++ b/src/stats-store.js @@ -1,6 +1,6 @@ import { Recordable } from './recordable.js' -class RecordableRow { +class StatsStore { constructor(...args) { Object.assign(this, args.reduce((acc, name) => { return { ...acc, [name]: new Recordable({ name }) } @@ -20,4 +20,4 @@ class RecordableRow { } } -export { RecordableRow } +export { StatsStore } diff --git a/src/stats-view.js b/src/stats-view.js new file mode 100644 index 0000000..70c291e --- /dev/null +++ b/src/stats-view.js @@ -0,0 +1,36 @@ +class StatsView { + constructor(lists, { + title = '', + subtitle = '', + updateInterval = 500 + } = {}) { + this.title = title + this.subtitle = subtitle + this.lists = lists + + this.timer = setInterval(this.render.bind(this), updateInterval) + } + + render() { + console.clear() + + console.log('\n') + + this.title + ? console.log('Title:', this.title) + : null + + this.lists.forEach(list => { + list.render() + console.log('\n') + }) + + console.log('\n') + } + + stop() { + clearInterval(this.timer) + } +} + +export { StatsView } From 516d98d31f5ac368f8767b144e35b8e0979c7b16 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 15:07:01 +0300 Subject: [PATCH 17/19] refactor: use the row concept --- src/stats-list.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stats-list.js b/src/stats-list.js index 11e09fc..9e015b4 100644 --- a/src/stats-list.js +++ b/src/stats-list.js @@ -6,7 +6,9 @@ class StatsList { ['foo.mean', 'Mean Task Duration (ms)'], ['bar.count', 'Total Count'] ] - } = {}) { + } = {}, statsStore = null) { + this.statsStore = statsStore + this.title = title this.subtitle = subtitle this.maxRows = maxRows @@ -17,6 +19,10 @@ class StatsList { } render() { + const rows = this.statsStore + ? this.statsStore.toClampedAverages(this.maxRows), + : this.rows.slice(this.rows.length - this.maxRows, this.rows.length) + const values = this.rows .sort(this.sort) .slice(this.rows.length - this.maxRows, this.rows.length) From 30bf1fae595fce3c90d7835aad41c61060d52354 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 15:10:21 +0300 Subject: [PATCH 18/19] refactor: use the row concept --- src/stats-list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats-list.js b/src/stats-list.js index 9e015b4..f73d55e 100644 --- a/src/stats-list.js +++ b/src/stats-list.js @@ -20,7 +20,7 @@ class StatsList { render() { const rows = this.statsStore - ? this.statsStore.toClampedAverages(this.maxRows), + ? this.statsStore.toClampedAverages(this.maxRows) : this.rows.slice(this.rows.length - this.maxRows, this.rows.length) const values = this.rows From 3d3528eac7dbc0cd9d25c2f8c7a2615d86bb11b7 Mon Sep 17 00:00:00 2001 From: Nicholas Kyriakides Date: Tue, 23 Jul 2024 15:12:42 +0300 Subject: [PATCH 19/19] refactor: use the row concept --- src/stats-list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stats-list.js b/src/stats-list.js index f73d55e..7fce5d4 100644 --- a/src/stats-list.js +++ b/src/stats-list.js @@ -20,7 +20,7 @@ class StatsList { render() { const rows = this.statsStore - ? this.statsStore.toClampedAverages(this.maxRows) + ? [this.statsStore.getRow()] : this.rows.slice(this.rows.length - this.maxRows, this.rows.length) const values = this.rows