mirror of
https://github.com/HeyPuter/puter.git
synced 2026-01-05 20:50:22 -06:00
bench: extract and benchmark CircularQueue
- Move CircularQueue class from AntiCSRFService to util directory - Add benchmark to ensure we don't slow it down
This commit is contained in:
@@ -20,76 +20,7 @@ const eggspress = require('../../api/eggspress');
|
||||
const config = require('../../config');
|
||||
const { subdomain } = require('../../helpers');
|
||||
const BaseService = require('../BaseService');
|
||||
|
||||
/**
|
||||
* A utility class used by AntiCSRFService to manage a circular queue of
|
||||
* CSRF tokens (or, as we like to call them, "anti-CSRF" tokens).
|
||||
*
|
||||
* A token expires when it is evicted from the queue.
|
||||
*/
|
||||
class CircularQueue {
|
||||
/**
|
||||
* Creates a new CircularQueue instance with the specified size.
|
||||
*
|
||||
* @param {number} size - The maximum number of items the queue can hold
|
||||
*/
|
||||
constructor (size) {
|
||||
this.size = size;
|
||||
this.queue = [];
|
||||
this.index = 0;
|
||||
this.map = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the queue. If the queue is full, the oldest item is removed.
|
||||
*
|
||||
* @param {*} item - The item to add to the queue
|
||||
*/
|
||||
push (item) {
|
||||
if ( this.queue[this.index] ) {
|
||||
this.map.delete(this.queue[this.index]);
|
||||
}
|
||||
this.queue[this.index] = item;
|
||||
this.map.set(item, this.index);
|
||||
this.index = (this.index + 1) % this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an item from the queue at the specified relative index.
|
||||
*
|
||||
* @param {number} index - The relative index from the current position
|
||||
* @returns {*} The item at the specified index
|
||||
*/
|
||||
get (index) {
|
||||
return this.queue[(this.index + index) % this.size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the queue contains the specified item.
|
||||
*
|
||||
* @param {*} item - The item to check for
|
||||
* @returns {boolean} True if the item exists in the queue, false otherwise
|
||||
*/
|
||||
has (item) {
|
||||
return this.map.has(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to consume (remove) an item from the queue if it exists.
|
||||
*
|
||||
* @param {*} item - The item to consume
|
||||
* @returns {boolean} True if the item was found and consumed, false otherwise
|
||||
*/
|
||||
maybe_consume (item) {
|
||||
if ( this.has(item) ) {
|
||||
const index = this.map.get(item);
|
||||
this.map.delete(item);
|
||||
this.queue[index] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const { CircularQueue } = require('../../util/CircularQueue');
|
||||
|
||||
/**
|
||||
* Class AntiCSRFService extends BaseService to manage and protect against Cross-Site Request Forgery (CSRF) attacks.
|
||||
|
||||
181
src/backend/src/util/CircularQueue.bench.js
Normal file
181
src/backend/src/util/CircularQueue.bench.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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 { CircularQueue } = require('./CircularQueue');
|
||||
|
||||
/**
|
||||
* Naive array-based implementation for comparison (no Map optimization).
|
||||
* This serves as a baseline to demonstrate the performance improvement
|
||||
* of the Map-optimized CircularQueue.
|
||||
*/
|
||||
class NaiveCircularQueue {
|
||||
constructor (size) {
|
||||
this.size = size;
|
||||
this.queue = [];
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
push (item) {
|
||||
this.queue[this.index] = item;
|
||||
this.index = (this.index + 1) % this.size;
|
||||
}
|
||||
|
||||
get (index) {
|
||||
return this.queue[(this.index + index) % this.size];
|
||||
}
|
||||
|
||||
has (item) {
|
||||
return this.queue.includes(item);
|
||||
}
|
||||
|
||||
maybe_consume (item) {
|
||||
const index = this.queue.indexOf(item);
|
||||
if ( index !== -1 ) {
|
||||
this.queue[index] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate test tokens
|
||||
const generateToken = () => Math.random().toString(36).substring(2, 15);
|
||||
|
||||
describe('CircularQueue - push() operations', () => {
|
||||
bench('push() with size=50', () => {
|
||||
const queue = new CircularQueue(50);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.push(generateToken());
|
||||
}
|
||||
});
|
||||
|
||||
bench('push() with size=500', () => {
|
||||
const queue = new CircularQueue(500);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.push(generateToken());
|
||||
}
|
||||
});
|
||||
|
||||
bench('NaiveCircularQueue push() with size=50 (baseline)', () => {
|
||||
const queue = new NaiveCircularQueue(50);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.push(generateToken());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CircularQueue - has() operations', () => {
|
||||
const setupQueue = (QueueClass, size) => {
|
||||
const queue = new QueueClass(size);
|
||||
const tokens = [];
|
||||
for ( let i = 0; i < size; i++ ) {
|
||||
const token = generateToken();
|
||||
tokens.push(token);
|
||||
queue.push(token);
|
||||
}
|
||||
return { queue, tokens };
|
||||
};
|
||||
|
||||
bench('has() on existing items - CircularQueue', () => {
|
||||
const { queue, tokens } = setupQueue(CircularQueue, 100);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.has(tokens[i % tokens.length]);
|
||||
}
|
||||
});
|
||||
|
||||
bench('has() on existing items - NaiveCircularQueue (baseline)', () => {
|
||||
const { queue, tokens } = setupQueue(NaiveCircularQueue, 100);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.has(tokens[i % tokens.length]);
|
||||
}
|
||||
});
|
||||
|
||||
bench('has() on non-existing items - CircularQueue', () => {
|
||||
const { queue } = setupQueue(CircularQueue, 100);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.has(`nonexistent-token-${ i}`);
|
||||
}
|
||||
});
|
||||
|
||||
bench('has() on non-existing items - NaiveCircularQueue (baseline)', () => {
|
||||
const { queue } = setupQueue(NaiveCircularQueue, 100);
|
||||
for ( let i = 0; i < 1000; i++ ) {
|
||||
queue.has(`nonexistent-token-${ i}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CircularQueue - maybe_consume() operations', () => {
|
||||
bench('maybe_consume() on existing items', () => {
|
||||
const queue = new CircularQueue(100);
|
||||
const tokens = [];
|
||||
for ( let i = 0; i < 100; i++ ) {
|
||||
const token = generateToken();
|
||||
tokens.push(token);
|
||||
queue.push(token);
|
||||
}
|
||||
for ( const token of tokens ) {
|
||||
queue.maybe_consume(token);
|
||||
}
|
||||
});
|
||||
|
||||
bench('maybe_consume() mixed existing/non-existing', () => {
|
||||
const queue = new CircularQueue(100);
|
||||
const tokens = [];
|
||||
for ( let i = 0; i < 100; i++ ) {
|
||||
const token = generateToken();
|
||||
tokens.push(token);
|
||||
queue.push(token);
|
||||
}
|
||||
for ( let i = 0; i < 200; i++ ) {
|
||||
if ( i % 2 === 0 && i / 2 < tokens.length ) {
|
||||
queue.maybe_consume(tokens[i / 2]);
|
||||
} else {
|
||||
queue.maybe_consume(`fake-token-${ i}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('CircularQueue - real-world usage pattern', () => {
|
||||
bench('CSRF token lifecycle: generate, validate, consume', () => {
|
||||
const queue = new CircularQueue(50);
|
||||
const activeTokens = [];
|
||||
|
||||
for ( let i = 0; i < 500; i++ ) {
|
||||
// Generate new token
|
||||
const token = generateToken();
|
||||
queue.push(token);
|
||||
activeTokens.push(token);
|
||||
|
||||
// Occasionally validate tokens
|
||||
if ( i % 3 === 0 && activeTokens.length > 0 ) {
|
||||
const checkToken = activeTokens[Math.floor(Math.random() * activeTokens.length)];
|
||||
queue.has(checkToken);
|
||||
}
|
||||
|
||||
// Occasionally consume tokens
|
||||
if ( i % 5 === 0 && activeTokens.length > 0 ) {
|
||||
const consumeToken = activeTokens.shift();
|
||||
queue.maybe_consume(consumeToken);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
93
src/backend/src/util/CircularQueue.js
Normal file
93
src/backend/src/util/CircularQueue.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A utility class to manage a circular queue with O(1) lookup.
|
||||
* Uses a Map for fast membership checks and a circular array for storage.
|
||||
*
|
||||
* Items expire when they are evicted from the queue (when the queue is full
|
||||
* and a new item is pushed).
|
||||
*/
|
||||
class CircularQueue {
|
||||
/**
|
||||
* Creates a new CircularQueue instance with the specified size.
|
||||
*
|
||||
* @param {number} size - The maximum number of items the queue can hold
|
||||
*/
|
||||
constructor (size) {
|
||||
this.size = size;
|
||||
this.queue = [];
|
||||
this.index = 0;
|
||||
this.map = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an item to the queue. If the queue is full, the oldest item is removed.
|
||||
*
|
||||
* @param {*} item - The item to add to the queue
|
||||
*/
|
||||
push (item) {
|
||||
if ( this.queue[this.index] ) {
|
||||
this.map.delete(this.queue[this.index]);
|
||||
}
|
||||
this.queue[this.index] = item;
|
||||
this.map.set(item, this.index);
|
||||
this.index = (this.index + 1) % this.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an item from the queue at the specified relative index.
|
||||
*
|
||||
* @param {number} index - The relative index from the current position
|
||||
* @returns {*} The item at the specified index
|
||||
*/
|
||||
get (index) {
|
||||
return this.queue[(this.index + index) % this.size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the queue contains the specified item.
|
||||
*
|
||||
* @param {*} item - The item to check for
|
||||
* @returns {boolean} True if the item exists in the queue, false otherwise
|
||||
*/
|
||||
has (item) {
|
||||
return this.map.has(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to consume (remove) an item from the queue if it exists.
|
||||
*
|
||||
* @param {*} item - The item to consume
|
||||
* @returns {boolean} True if the item was found and consumed, false otherwise
|
||||
*/
|
||||
maybe_consume (item) {
|
||||
if ( this.has(item) ) {
|
||||
const index = this.map.get(item);
|
||||
this.map.delete(item);
|
||||
this.queue[index] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CircularQueue,
|
||||
};
|
||||
Reference in New Issue
Block a user