mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-04 00:20:45 +00:00
perf: more benchmarks
This commit is contained in:
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user