blob: 11e32e00e9219677f53a1fd330a7fd03dd3100bc [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { logger } from "./Logger";
/**
* Buffer pool for reusing buffers to reduce GC pressure
* Inspired by pg nodejs client's buffer management strategy
*
* Key design principles:
* 1. Size classes to minimize waste (powers of 2)
* 2. Maximum pool size to prevent memory bloat
* 3. Clear statistics for monitoring
*/
export class BufferPool {
// Size classes: 1KB, 4KB, 16KB, 64KB, 256KB, 1MB, 4MB
private static readonly SIZE_CLASSES = [
1024, // 1KB
4096, // 4KB
16384, // 16KB
65536, // 64KB
262144, // 256KB
1048576, // 1MB
4194304, // 4MB
];
// Maximum buffers per size class
private static readonly MAX_BUFFERS_PER_CLASS = 10;
// Pools organized by size class
private pools: Map<number, Buffer[]>;
// Statistics
private stats = {
hits: 0,
misses: 0,
allocations: 0,
returns: 0,
};
constructor() {
this.pools = new Map();
for (const size of BufferPool.SIZE_CLASSES) {
this.pools.set(size, []);
}
}
/**
* Get a buffer of at least the requested size
* Returns a pooled buffer if available, otherwise allocates new
* @throws Error if buffer allocation fails (e.g., out of memory)
*/
acquire(minSize: number): Buffer {
// Find appropriate size class
const sizeClass = this.getSizeClass(minSize);
try {
if (sizeClass === null) {
// Size too large for pooling, allocate directly
this.stats.misses++;
this.stats.allocations++;
return Buffer.allocUnsafe(minSize);
}
const pool = this.pools.get(sizeClass);
if (!pool) {
this.stats.misses++;
this.stats.allocations++;
return Buffer.allocUnsafe(sizeClass);
}
const buffer = pool.pop();
if (buffer) {
this.stats.hits++;
// Return the full buffer from the pool (will be sized to sizeClass)
// The caller is responsible for using only minSize bytes
return buffer.subarray(0, minSize);
} else {
this.stats.misses++;
this.stats.allocations++;
return Buffer.allocUnsafe(sizeClass);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Buffer allocation failed for size ${minSize}: ${message}`);
}
}
/**
* Return a buffer to the pool for reuse
* Only pools buffers that fit in size classes
*/
release(buffer: Buffer): void {
const sizeClass = this.getSizeClass(buffer.length);
if (sizeClass === null) {
// Buffer too large for pooling, let GC handle it
return;
}
const pool = this.pools.get(sizeClass);
if (!pool) {
return;
}
// Only pool if we haven't hit the limit
if (pool.length < BufferPool.MAX_BUFFERS_PER_CLASS) {
// Only pool if it matches the size class exactly
if (buffer.length === sizeClass) {
pool.push(buffer);
this.stats.returns++;
}
}
}
/**
* Find the appropriate size class for a requested size
* Returns null if size is too large for pooling
*/
private getSizeClass(size: number): number | null {
for (const sizeClass of BufferPool.SIZE_CLASSES) {
if (size <= sizeClass) {
return sizeClass;
}
}
return null; // Too large for pooling
}
/**
* Get pool statistics
*/
getStats() {
const hitRate = this.stats.hits + this.stats.misses > 0
? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2)
: '0.00';
const pooledBuffers = Array.from(this.pools.values()).reduce(
(sum, pool) => sum + pool.length,
0
);
return {
...this.stats,
hitRate: `${hitRate}%`,
pooledBuffers,
};
}
/**
* Clear all pooled buffers
*/
clear(): void {
for (const pool of this.pools.values()) {
pool.length = 0;
}
logger.debug('BufferPool cleared');
}
/**
* Log statistics (for debugging)
*/
logStats(): void {
const stats = this.getStats();
logger.debug(`BufferPool stats: ${JSON.stringify(stats)}`);
}
}
// Global singleton instance
export const globalBufferPool = new BufferPool();