From d3c1fc31035eba74aff516723389febecf8f870a Mon Sep 17 00:00:00 2001 From: KernelDeimos <7225168+KernelDeimos@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:13:02 -0500 Subject: [PATCH] perf: more benchmarks --- .../services/file-cache/FileTracker.bench.js | 182 ++++++++++++++++++ src/backend/src/util/datautil.bench.js | 165 ++++++++++++++++ src/backend/src/util/opmath.bench.js | 122 ++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 src/backend/src/services/file-cache/FileTracker.bench.js create mode 100644 src/backend/src/util/datautil.bench.js create mode 100644 src/backend/src/util/opmath.bench.js diff --git a/src/backend/src/services/file-cache/FileTracker.bench.js b/src/backend/src/services/file-cache/FileTracker.bench.js new file mode 100644 index 000000000..0e6a517a7 --- /dev/null +++ b/src/backend/src/services/file-cache/FileTracker.bench.js @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { bench, describe } from 'vitest'; +const { FileTracker } = require('./FileTracker'); + +// Helper to create a tracker with some access history +const createTrackerWithHistory = (accessCount) => { + const tracker = new FileTracker({ key: 'test-key', size: 1024 }); + for ( let i = 0; i < accessCount; i++ ) { + tracker.touch(); + } + return tracker; +}; + +describe('FileTracker - Construction', () => { + bench('create new FileTracker', () => { + new FileTracker({ key: `test-key-${ Math.random()}`, size: 1024 }); + }); + + bench('create multiple FileTrackers', () => { + for ( let i = 0; i < 100; i++ ) { + new FileTracker({ key: `key-${i}`, size: i * 100 }); + } + }); +}); + +describe('FileTracker - touch() operation', () => { + bench('touch() on new tracker', () => { + const tracker = new FileTracker({ key: 'test', size: 1024 }); + for ( let i = 0; i < 1000; i++ ) { + tracker.touch(); + } + }); + + bench('touch() with EWMA calculation', () => { + const tracker = new FileTracker({ key: 'test', size: 1024 }); + // Pre-warm with some touches + for ( let i = 0; i < 10; i++ ) { + tracker.touch(); + } + // Benchmark steady-state touches + for ( let i = 0; i < 1000; i++ ) { + tracker.touch(); + } + }); +}); + +describe('FileTracker - score calculation', () => { + bench('score on fresh tracker', () => { + const tracker = new FileTracker({ key: 'test', size: 1024 }); + tracker.touch(); // Need at least one touch for meaningful score + for ( let i = 0; i < 1000; i++ ) { + void tracker.score; + } + }); + + bench('score on tracker with history (10 accesses)', () => { + const tracker = createTrackerWithHistory(10); + for ( let i = 0; i < 1000; i++ ) { + void tracker.score; + } + }); + + bench('score on tracker with history (100 accesses)', () => { + const tracker = createTrackerWithHistory(100); + for ( let i = 0; i < 1000; i++ ) { + void tracker.score; + } + }); +}); + +describe('FileTracker - age calculation', () => { + bench('age getter', () => { + const tracker = new FileTracker({ key: 'test', size: 1024 }); + for ( let i = 0; i < 10000; i++ ) { + void tracker.age; + } + }); +}); + +describe('FileTracker - Cache eviction simulation', () => { + bench('compare scores of multiple trackers', () => { + // Simulate cache with 100 items + const trackers = []; + for ( let i = 0; i < 100; i++ ) { + const tracker = new FileTracker({ key: `file-${i}`, size: i * 100 }); + // Simulate varying access patterns + const accessCount = Math.floor(Math.random() * 20); + for ( let j = 0; j < accessCount; j++ ) { + tracker.touch(); + } + trackers.push(tracker); + } + + // Find lowest score (eviction candidate) + for ( let i = 0; i < 100; i++ ) { + let minScore = Infinity; + let evictCandidate = null; + for ( const tracker of trackers ) { + const score = tracker.score; + if ( score < minScore ) { + minScore = score; + evictCandidate = tracker; + } + } + } + }); + + bench('sort trackers by score (eviction ordering)', () => { + const trackers = []; + for ( let i = 0; i < 50; i++ ) { + const tracker = new FileTracker({ key: `file-${i}`, size: i * 100 }); + for ( let j = 0; j < i % 10; j++ ) { + tracker.touch(); + } + trackers.push(tracker); + } + + // Sort by score + for ( let i = 0; i < 10; i++ ) { + [...trackers].sort((a, b) => a.score - b.score); + } + }); +}); + +describe('FileTracker - Real-world access patterns', () => { + bench('hot file pattern (frequent access)', () => { + const tracker = new FileTracker({ key: 'hot-file', size: 1024 }); + for ( let i = 0; i < 1000; i++ ) { + tracker.touch(); + if ( i % 10 === 0 ) { + void tracker.score; + } + } + }); + + bench('cold file pattern (rare access)', () => { + const tracker = new FileTracker({ key: 'cold-file', size: 1024 }); + tracker.touch(); + for ( let i = 0; i < 1000; i++ ) { + void tracker.score; + void tracker.age; + } + }); + + bench('mixed access with score checks', () => { + const trackers = []; + for ( let i = 0; i < 20; i++ ) { + trackers.push(new FileTracker({ key: `file-${i}`, size: 1024 })); + } + + for ( let i = 0; i < 500; i++ ) { + // Random access + const idx = Math.floor(Math.random() * trackers.length); + trackers[idx].touch(); + + // Periodic eviction check + if ( i % 50 === 0 ) { + for ( const t of trackers ) { + void t.score; + } + } + } + }); +}); diff --git a/src/backend/src/util/datautil.bench.js b/src/backend/src/util/datautil.bench.js new file mode 100644 index 000000000..f3f21366c --- /dev/null +++ b/src/backend/src/util/datautil.bench.js @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { bench, describe } from 'vitest'; +const { stringify_serializable_object, hash_serializable_object } = require('./datautil'); + +// Test data generators +const createFlatObject = (size) => { + const obj = {}; + for ( let i = 0; i < size; i++ ) { + obj[`key${i}`] = `value${i}`; + } + return obj; +}; + +const createNestedObject = (depth, breadth) => { + if ( depth === 0 ) { + return { leaf: 'value' }; + } + const obj = {}; + for ( let i = 0; i < breadth; i++ ) { + obj[`level${depth}_child${i}`] = createNestedObject(depth - 1, breadth); + } + return obj; +}; + +const createMixedObject = () => ({ + string: 'hello world', + number: 42, + boolean: true, + null: null, + array: [1, 2, 3, { nested: 'array' }], + nested: { + deep: { + value: 'found', + numbers: [1, 2, 3], + }, + }, +}); + +// Objects with different key orderings (should produce same hash) +const objA = { z: 1, a: 2, m: 3 }; +const objB = { a: 2, m: 3, z: 1 }; +const objC = { m: 3, z: 1, a: 2 }; + +describe('stringify_serializable_object - Flat objects', () => { + const small = createFlatObject(5); + const medium = createFlatObject(20); + const large = createFlatObject(100); + + bench('small flat object (5 keys)', () => { + stringify_serializable_object(small); + }); + + bench('medium flat object (20 keys)', () => { + stringify_serializable_object(medium); + }); + + bench('large flat object (100 keys)', () => { + stringify_serializable_object(large); + }); +}); + +describe('stringify_serializable_object - Nested objects', () => { + const shallow = createNestedObject(2, 3); // depth 2, 3 children each + const medium = createNestedObject(3, 3); // depth 3, 3 children each + const deep = createNestedObject(4, 2); // depth 4, 2 children each + + bench('shallow nested (depth=2, breadth=3)', () => { + stringify_serializable_object(shallow); + }); + + bench('medium nested (depth=3, breadth=3)', () => { + stringify_serializable_object(medium); + }); + + bench('deep nested (depth=4, breadth=2)', () => { + stringify_serializable_object(deep); + }); +}); + +describe('stringify_serializable_object - Mixed types', () => { + const mixed = createMixedObject(); + + bench('mixed type object', () => { + stringify_serializable_object(mixed); + }); + + bench('primitives', () => { + stringify_serializable_object('string'); + stringify_serializable_object(42); + stringify_serializable_object(true); + stringify_serializable_object(null); + stringify_serializable_object(undefined); + }); +}); + +describe('stringify_serializable_object - Key ordering normalization', () => { + bench('objects with different key orderings', () => { + // All should produce the same output + stringify_serializable_object(objA); + stringify_serializable_object(objB); + stringify_serializable_object(objC); + }); +}); + +describe('stringify_serializable_object vs JSON.stringify', () => { + const obj = createFlatObject(20); + + bench('stringify_serializable_object', () => { + stringify_serializable_object(obj); + }); + + bench('JSON.stringify (baseline, no key sorting)', () => { + JSON.stringify(obj); + }); + + bench('JSON.stringify with sorted keys (manual)', () => { + const sortedObj = {}; + Object.keys(obj).sort().forEach(k => { + sortedObj[k] = obj[k]; + }); + JSON.stringify(sortedObj); + }); +}); + +describe('hash_serializable_object', () => { + const small = createFlatObject(5); + const medium = createFlatObject(20); + const mixed = createMixedObject(); + + bench('hash small object', () => { + hash_serializable_object(small); + }); + + bench('hash medium object', () => { + hash_serializable_object(medium); + }); + + bench('hash mixed object', () => { + hash_serializable_object(mixed); + }); + + bench('hash objects with different key orderings (should be equal)', () => { + hash_serializable_object(objA); + hash_serializable_object(objB); + hash_serializable_object(objC); + }); +}); diff --git a/src/backend/src/util/opmath.bench.js b/src/backend/src/util/opmath.bench.js new file mode 100644 index 000000000..c37000c88 --- /dev/null +++ b/src/backend/src/util/opmath.bench.js @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { bench, describe } from 'vitest'; +const { EWMA, MovingMode, TimeWindow, normalize } = require('./opmath'); + +describe('EWMA - Exponential Weighted Moving Average', () => { + bench('EWMA put() with constant alpha', () => { + const ewma = new EWMA({ initial: 0, alpha: 0.2 }); + for ( let i = 0; i < 1000; i++ ) { + ewma.put(Math.random() * 100); + } + }); + + bench('EWMA put() with function alpha', () => { + const ewma = new EWMA({ initial: 0, alpha: () => 0.2 }); + for ( let i = 0; i < 1000; i++ ) { + ewma.put(Math.random() * 100); + } + }); + + bench('EWMA get() after many puts', () => { + const ewma = new EWMA({ initial: 0, alpha: 0.2 }); + for ( let i = 0; i < 100; i++ ) { + ewma.put(i); + } + for ( let i = 0; i < 1000; i++ ) { + ewma.get(); + } + }); +}); + +describe('MovingMode - Mode calculation with sliding window', () => { + bench('MovingMode put() with window_size=30', () => { + const mode = new MovingMode({ initial: 0, window_size: 30 }); + for ( let i = 0; i < 1000; i++ ) { + mode.put(Math.floor(Math.random() * 10)); + } + }); + + bench('MovingMode put() with window_size=100', () => { + const mode = new MovingMode({ initial: 0, window_size: 100 }); + for ( let i = 0; i < 1000; i++ ) { + mode.put(Math.floor(Math.random() * 10)); + } + }); + + bench('MovingMode with high cardinality values', () => { + const mode = new MovingMode({ initial: 0, window_size: 50 }); + for ( let i = 0; i < 1000; i++ ) { + mode.put(Math.floor(Math.random() * 1000)); + } + }); + + bench('MovingMode with low cardinality values', () => { + const mode = new MovingMode({ initial: 0, window_size: 50 }); + for ( let i = 0; i < 1000; i++ ) { + mode.put(Math.floor(Math.random() * 3)); + } + }); +}); + +describe('TimeWindow - Time-based sliding window', () => { + bench('TimeWindow add() and get()', () => { + let fakeTime = 0; + const tw = new TimeWindow({ + window_duration: 1000, + reducer: values => values.reduce((a, b) => a + b, 0), + now: () => fakeTime, + }); + for ( let i = 0; i < 1000; i++ ) { + fakeTime += 10; + tw.add(Math.random()); + } + }); + + bench('TimeWindow with stale entry removal', () => { + let fakeTime = 0; + const tw = new TimeWindow({ + window_duration: 100, + reducer: values => values.length, + now: () => fakeTime, + }); + for ( let i = 0; i < 1000; i++ ) { + fakeTime += 50; // Fast time progression causes stale removal + tw.add(i); + tw.get(); + } + }); +}); + +describe('normalize - Exponential normalization', () => { + bench('normalize() single value', () => { + for ( let i = 0; i < 10000; i++ ) { + normalize({ high_value: 0.001 }, Math.random()); + } + }); + + bench('normalize() with varying high_value', () => { + const high_values = [0.001, 0.01, 0.1, 1, 10]; + for ( let i = 0; i < 10000; i++ ) { + const hv = high_values[i % high_values.length]; + normalize({ high_value: hv }, Math.random() * 100); + } + }); +});