Last active
October 10, 2025 20:43
-
-
Save corporatepiyush/4f86f503e95482cd211388d4e00b8646 to your computer and use it in GitHub Desktop.
High Performace Application level Buffer Pool for Networking, Image Processing, Graphics, Game engines and File IO Tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// @file A high-performance buffer pool implementation in Dart for managing TypedData. | |
| /// @author Piyush Katariya | |
| /// @version 1.5.0 | |
| library buffer_pool; | |
| import 'dart:typed_data'; | |
| import 'dart:math'; | |
| import 'dart:ffi'; | |
| import 'package:ffi/ffi.dart'; | |
| /// A function signature for creating a new [TypedData] instance of a specific length. | |
| typedef TypedDataCreator<T extends TypedData> = T Function(int length); | |
| /// A function signature for creating a view on a [ByteBuffer]. | |
| typedef TypedDataViewCreator<T extends TypedData> = | |
| T Function(ByteBuffer buffer, int offsetInBytes, int length); | |
| /// An enum defining fixed allocation capacities for buffer pool segments. | |
| /// | |
| /// These values determine the initial number of segments created within a slab. | |
| enum Capacity { | |
| Initial(1), | |
| Quarter(16), | |
| Half(32), | |
| Distinct(48), | |
| Full(64); | |
| const Capacity(this.value); | |
| final int value; | |
| } | |
| /// Configuration class for buffer pool initialization. | |
| class PoolConfig { | |
| final int minSize; | |
| final int maxSize; | |
| final Capacity capacity; | |
| final List<int>? bucketRange; | |
| final List<int>? segmentsPerBucket; | |
| final List<int>? buffersPerSegment; | |
| const PoolConfig({ | |
| this.minSize = 16, | |
| this.maxSize = 256 * 1024, | |
| this.capacity = Capacity.Initial, | |
| this.bucketRange, | |
| this.segmentsPerBucket, | |
| this.buffersPerSegment, | |
| }); | |
| /// Validates the configuration parameters. | |
| void validate() { | |
| if (minSize < 16) throw Exception('minSize cannot be less than 16.'); | |
| if (maxSize > 16 * 1024 * 1024) { | |
| throw Exception('maxSize cannot be greater than 16 MB.'); | |
| } | |
| if (minSize >= maxSize) | |
| throw Exception('minSize must be less than maxSize.'); | |
| if (bucketRange != null || | |
| segmentsPerBucket != null || | |
| buffersPerSegment != null) { | |
| if (bucketRange == null || | |
| segmentsPerBucket == null || | |
| buffersPerSegment == null) { | |
| throw Exception( | |
| 'All custom slab configuration arrays must be provided together.', | |
| ); | |
| } | |
| if (bucketRange!.length != segmentsPerBucket!.length || | |
| bucketRange!.length != buffersPerSegment!.length) { | |
| throw Exception( | |
| 'All arrays in custom slab config must have the same length.', | |
| ); | |
| } | |
| if (bucketRange!.length > BufferPool.maxSlabs) { | |
| throw Exception( | |
| 'The length of arrays in custom slab config cannot exceed the maximum of ${BufferPool.maxSlabs}.', | |
| ); | |
| } | |
| for (final bucket in bucketRange!) { | |
| if (bucket <= 0) { | |
| throw Exception( | |
| 'All bucket sizes in custom slab config must be positive numbers.', | |
| ); | |
| } | |
| } | |
| for (int i = 0; i < bucketRange!.length - 1; i++) { | |
| if (bucketRange![i] >= bucketRange![i + 1]) { | |
| throw Exception( | |
| 'bucketRange in custom slab config must be sorted in ascending order and contain no duplicate values.', | |
| ); | |
| } | |
| } | |
| for (final segmentCount in segmentsPerBucket!) { | |
| if (segmentCount > BufferPool.maxSegments || segmentCount <= 0) { | |
| throw Exception( | |
| 'The number of segments per bucket in custom slab config must be between 1 and ${BufferPool.maxSegments}.', | |
| ); | |
| } | |
| } | |
| for (final buffersCount in buffersPerSegment!) { | |
| if (buffersCount > BufferPool.maxBuffers || buffersCount <= 0) { | |
| throw Exception( | |
| 'The number of buffers per segment in custom slab config must be between 1 and ${BufferPool.maxBuffers}.', | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// Abstract base class for buffers managed by the pool. | |
| abstract class IBuffer<T extends TypedData> { | |
| /// The TypedData view of the buffer's data. | |
| T get data; | |
| /// A packed integer containing metadata for deallocation. Will be 0 for non-pooled buffers. | |
| int get meta; | |
| /// A flag indicating if the buffer is managed by the pool. | |
| bool get isPooled; | |
| } | |
| /// Represents a memory buffer that is a view into a larger slab segment. | |
| /// | |
| /// This class uses bit-packing to efficiently store metadata (slab, segment, | |
| /// and buffer indices) within a single 16-bit number, which is crucial for | |
| /// quick deallocation. | |
| class PooledBuffer<T extends TypedData> implements IBuffer<T> { | |
| @override | |
| final T data; | |
| @override | |
| final int meta; | |
| @override | |
| final bool isPooled = true; | |
| // Adjusted masks: slab(4) + segment(6) + buffer(5) = 15 bits | |
| // Slab: bits 11-14 (4 bits for 0-15) | |
| static const int _slabMask = 0x7800; // 0111100000000000 | |
| // Segment: bits 5-10 (6 bits for 0-63) | |
| static const int _segmentMask = 0x07E0; // 0000011111100000 | |
| // Buffer: bits 0-4 (5 bits for 0-31) | |
| static const int _bufferMask = 0x001F; // 0000000000011111 | |
| static const int _slabShift = 11; | |
| static const int _segmentShift = 5; | |
| /// Creates a pooled buffer instance. | |
| PooledBuffer( | |
| int slabIndex, | |
| int segmentIndex, | |
| int bufferIndex, | |
| int elementCount, | |
| T segmentData, | |
| TypedDataViewCreator<T> viewCreator, | |
| ) : meta = | |
| (slabIndex << _slabShift) | | |
| (segmentIndex << _segmentShift) | | |
| bufferIndex, | |
| data = viewCreator( | |
| segmentData.buffer, | |
| bufferIndex * elementCount * segmentData.elementSizeInBytes, | |
| elementCount, | |
| ); | |
| /// Decodes the slab index from the packed metadata. | |
| @pragma('vm:prefer-inline') | |
| int get slabIndex => (meta & _slabMask) >> _slabShift; | |
| /// Decodes the segment index from the packed metadata. | |
| @pragma('vm:prefer-inline') | |
| int get segmentIndex => (meta & _segmentMask) >> _segmentShift; | |
| /// Decodes the buffer index from the packed metadata. | |
| @pragma('vm:prefer-inline') | |
| int get bufferIndex => meta & _bufferMask; | |
| } | |
| /// Represents a buffer that is not managed by the buffer pool. | |
| /// | |
| /// This class follows the null object pattern, with `meta` always as 0, | |
| /// making it easily distinguishable from a [PooledBuffer] during deallocation. | |
| class NonPooledBuffer<T extends TypedData> implements IBuffer<T> { | |
| @override | |
| final T data; | |
| @override | |
| final int meta = 0; | |
| @override | |
| final bool isPooled = false; | |
| /// Creates a non-pooled buffer instance. | |
| NonPooledBuffer(int elementCount, TypedDataCreator<T> creator) | |
| : data = creator(elementCount); | |
| } | |
| /// A Segment is a contiguous block of memory carved into fixed-size buffers. | |
| /// | |
| /// Each segment can hold up to 32 buffers of a specific size category (bucket). | |
| class Segment<T extends TypedData> { | |
| final int bucketElementCount; | |
| final T data; | |
| final List<PooledBuffer<T>> bufferPool; // Pre-allocated | |
| final List<int> freeIndices; // Track free buffer indices | |
| int freeCount; | |
| final int slabIndex; | |
| final int segmentIndex; | |
| final int maxBuffers; | |
| Segment( | |
| this.bucketElementCount, | |
| this.slabIndex, | |
| this.segmentIndex, | |
| this.maxBuffers, | |
| TypedDataCreator<T> creator, | |
| TypedDataViewCreator<T> viewCreator, | |
| ) : data = creator(bucketElementCount * maxBuffers), | |
| bufferPool = List.generate( | |
| maxBuffers, | |
| (i) => PooledBuffer<T>( | |
| slabIndex, | |
| segmentIndex, | |
| i, | |
| bucketElementCount, | |
| creator( | |
| bucketElementCount * maxBuffers, | |
| ), // Temporary, will be replaced | |
| viewCreator, | |
| ), | |
| growable: false, | |
| ), | |
| freeIndices = List.generate(maxBuffers, (i) => i, growable: false), | |
| freeCount = maxBuffers { | |
| // Re-create buffers with correct data reference | |
| for (int i = 0; i < maxBuffers; i++) { | |
| bufferPool[i] = PooledBuffer<T>( | |
| slabIndex, | |
| segmentIndex, | |
| i, | |
| bucketElementCount, | |
| data, | |
| viewCreator, | |
| ); | |
| } | |
| } | |
| /// Allocates a buffer from this segment. | |
| @pragma('vm:prefer-inline') | |
| IBuffer<T> allocate() { | |
| if (freeCount > 0) { | |
| freeCount--; | |
| return bufferPool[freeIndices[freeCount]]; | |
| } | |
| return NonPooledBuffer<T>( | |
| bucketElementCount, | |
| (len) => data.buffer.asUint8List(0, 0) as T, | |
| ); | |
| } | |
| /// Returns a buffer to this segment's pool. | |
| @pragma('vm:prefer-inline') | |
| void deallocate(int bufferIndex) { | |
| if (freeCount < maxBuffers) { | |
| freeIndices[freeCount] = bufferIndex; | |
| freeCount++; | |
| } | |
| } | |
| } | |
| /// A Slab is a fixed-size buffer pool that manages up to 64 segments. | |
| /// | |
| /// Each slab is responsible for a single buffer bucket (a specific size category). | |
| class Slab<T extends TypedData> { | |
| final int bucketElementCount; | |
| final List<Segment<T>> segments; | |
| final Set<int> freeSegmentSet; // O(1) lookup for free segments | |
| final int slabIndex; | |
| final int buffersPerSegment; | |
| final TypedDataCreator<T> _creator; | |
| final TypedDataViewCreator<T> _viewCreator; | |
| Slab( | |
| this.bucketElementCount, | |
| this.slabIndex, | |
| int initialCapacity, | |
| this.buffersPerSegment, | |
| this._creator, | |
| this._viewCreator, | |
| ) : segments = [], | |
| freeSegmentSet = {} { | |
| for (int i = 0; i < initialCapacity; i++) { | |
| _addSegment(); | |
| } | |
| } | |
| /// Adds a new segment to the slab if capacity allows. | |
| void _addSegment() { | |
| if (segments.length >= BufferPool.maxSegments) { | |
| throw Exception('Maximum number of segments reached for this slab.'); | |
| } | |
| final newSegment = Segment<T>( | |
| bucketElementCount, | |
| slabIndex, | |
| segments.length, | |
| buffersPerSegment, | |
| _creator, | |
| _viewCreator, | |
| ); | |
| segments.add(newSegment); | |
| freeSegmentSet.add(segments.length - 1); | |
| } | |
| /// Allocates a buffer from one of the slab's segments. | |
| @pragma('vm:prefer-inline') | |
| IBuffer<T> allocate() { | |
| if (freeSegmentSet.isEmpty && segments.length < BufferPool.maxSegments) { | |
| _addSegment(); | |
| } | |
| if (freeSegmentSet.isEmpty) { | |
| return NonPooledBuffer<T>(bucketElementCount, _creator); | |
| } | |
| final segmentIndex = freeSegmentSet.first; | |
| final segment = segments[segmentIndex]; | |
| final buffer = segment.allocate(); | |
| if (segment.freeCount == 0) { | |
| freeSegmentSet.remove(segmentIndex); | |
| } | |
| return buffer; | |
| } | |
| /// Deallocates a buffer, returning it to the appropriate segment. | |
| @pragma('vm:prefer-inline') | |
| void deallocate(PooledBuffer<T> buffer) { | |
| final segmentIndex = buffer.segmentIndex; | |
| if (segmentIndex >= 0 && segmentIndex < segments.length) { | |
| final segment = segments[segmentIndex]; | |
| final wasFull = segment.freeCount == 0; | |
| segment.deallocate(buffer.bufferIndex); | |
| if (wasFull) { | |
| freeSegmentSet.add(segmentIndex); | |
| } | |
| } | |
| } | |
| /// The total number of free buffers across all segments in this slab. | |
| int get freeCount => | |
| segments.fold(0, (total, segment) => total + segment.freeCount); | |
| } | |
| /// Manages multiple slabs, providing a unified interface for allocation and deallocation. | |
| class InternalBufferPool<T extends TypedData> { | |
| final List<Slab<T>> slabs; | |
| final List<int> buckets; | |
| final TypedDataCreator<T> _creator; | |
| InternalBufferPool( | |
| PoolConfig config, | |
| this._creator, | |
| TypedDataViewCreator<T> viewCreator, | |
| ) : slabs = [], | |
| buckets = [] { | |
| config.validate(); | |
| final int initialCapacity = config.capacity.value; | |
| late List<int> finalBucketRange; | |
| late List<int> finalSegmentsPerBucket; | |
| late List<int> finalBuffersPerSegment; | |
| if (config.bucketRange != null) { | |
| finalBucketRange = config.bucketRange!; | |
| finalSegmentsPerBucket = config.segmentsPerBucket!; | |
| finalBuffersPerSegment = config.buffersPerSegment!; | |
| } else { | |
| finalBucketRange = _generateBetterBuckets(config.minSize, config.maxSize); | |
| finalSegmentsPerBucket = List.filled( | |
| finalBucketRange.length, | |
| initialCapacity, | |
| ); | |
| finalBuffersPerSegment = List.filled( | |
| finalBucketRange.length, | |
| BufferPool.maxBuffers, | |
| ); | |
| } | |
| buckets.addAll(finalBucketRange); | |
| for (int i = 0; i < buckets.length; i++) { | |
| final bucket = buckets[i]; | |
| final segments = finalSegmentsPerBucket[i]; | |
| final buffers = finalBuffersPerSegment[i]; | |
| final slab = Slab<T>(bucket, i, segments, buffers, _creator, viewCreator); | |
| slabs.add(slab); | |
| } | |
| } | |
| /// Generates better distributed bucket sizes for common use cases | |
| static List<int> _generateBetterBuckets(int minSize, int maxSize) { | |
| final sizes = <int>{}; | |
| // Add power-of-2 sizes | |
| for (int power = 4; power <= 20; power++) { | |
| final base = 1 << power; | |
| if (base >= minSize && base <= maxSize) { | |
| sizes.add(base); | |
| } | |
| } | |
| // Add common small sizes | |
| for (int size = minSize; size <= min(256, maxSize); size += 16) { | |
| sizes.add(size); | |
| } | |
| // Ensure we have the min and max | |
| sizes.add(minSize); | |
| sizes.add(maxSize); | |
| final result = sizes.toList()..sort(); | |
| // Limit to maxSlabs (16) | |
| if (result.length > BufferPool.maxSlabs) { | |
| final step = result.length / BufferPool.maxSlabs; | |
| final filtered = <int>[]; | |
| for (int i = 0; i < BufferPool.maxSlabs; i++) { | |
| filtered.add(result[(i * step).floor()]); | |
| } | |
| return filtered; | |
| } | |
| return result; | |
| } | |
| /// Acquires a buffer that can hold at least the requested number of elements. | |
| @pragma('vm:prefer-inline') | |
| IBuffer<T> acquire(int requestedElementCount) { | |
| // Binary search for the appropriate bucket | |
| int low = 0; | |
| int high = buckets.length - 1; | |
| while (low <= high) { | |
| final mid = (low + high) >>> 1; | |
| final currentBucket = buckets[mid]; | |
| if (currentBucket >= requestedElementCount) { | |
| if (mid == 0 || buckets[mid - 1] < requestedElementCount) { | |
| return slabs[mid].allocate(); | |
| } | |
| high = mid - 1; | |
| } else { | |
| low = mid + 1; | |
| } | |
| } | |
| return NonPooledBuffer<T>(requestedElementCount, _creator); | |
| } | |
| /// Acquires a buffer and ensures its contents are zeroed out. | |
| IBuffer<T> acquireSafe(int requestedElementCount) { | |
| final buffer = acquire(requestedElementCount); | |
| if (buffer.isPooled) { | |
| final data = buffer.data; | |
| // Use fillRange which is optimized in Dart VM | |
| if (data is Uint8List) { | |
| data.fillRange(0, data.length, 0); | |
| } else if (data is Int8List) { | |
| data.fillRange(0, data.length, 0); | |
| } else if (data is Uint16List) { | |
| data.fillRange(0, data.length, 0); | |
| } else if (data is Int16List) { | |
| data.fillRange(0, data.length, 0); | |
| } else if (data is Uint32List) { | |
| data.fillRange(0, data.length, 0); | |
| } else if (data is Int32List) { | |
| data.fillRange(0, data.length, 0); | |
| } else if (data is Float32List) { | |
| data.fillRange(0, data.length, 0.0); | |
| } else if (data is Float64List) { | |
| data.fillRange(0, data.length, 0.0); | |
| } else { | |
| final byteData = data.buffer.asUint8List( | |
| data.offsetInBytes, | |
| data.lengthInBytes, | |
| ); | |
| byteData.fillRange(0, byteData.length, 0); | |
| } | |
| } | |
| return buffer; | |
| } | |
| /// Releases a buffer back to the pool. | |
| @pragma('vm:prefer-inline') | |
| void release(IBuffer<T> buffer) { | |
| if (buffer.isPooled && buffer is PooledBuffer<T>) { | |
| final slabIndex = buffer.slabIndex; | |
| if (slabIndex >= 0 && slabIndex < slabs.length) { | |
| slabs[slabIndex].deallocate(buffer); | |
| } | |
| } | |
| } | |
| /// The total number of free buffers across all slabs. | |
| int get freeCount => slabs.fold(0, (total, slab) => total + slab.freeCount); | |
| } | |
| /// The main public-facing class for the buffer pool. | |
| class BufferPool { | |
| /// Maximum number of slabs (bucket sizes) the pool can manage. | |
| static const int maxSlabs = 16; | |
| /// Maximum number of segments a single slab can manage. | |
| static const int maxSegments = 64; | |
| /// Maximum number of buffers a single segment can hold. | |
| static const int maxBuffers = 32; | |
| static final Map<Type, TypedDataCreator> _creatorRegistry = { | |
| Int8List: (int length) => Int8List(length), | |
| Uint8List: (int length) => Uint8List(length), | |
| Int16List: (int length) => Int16List(length), | |
| Uint16List: (int length) => Uint16List(length), | |
| Int32List: (int length) => Int32List(length), | |
| Uint32List: (int length) => Uint32List(length), | |
| Float32List: (int length) => Float32List(length), | |
| Float64List: (int length) => Float64List(length), | |
| }; | |
| static final Map<Type, TypedDataCreator> _heapCreatorRegistry = { | |
| Int8List: (int length) => malloc<Int8>(length).asTypedList(length), | |
| Uint8List: (int length) => malloc<Uint8>(length).asTypedList(length), | |
| Int16List: (int length) => malloc<Int16>(length).asTypedList(length), | |
| Uint16List: (int length) => malloc<Uint16>(length).asTypedList(length), | |
| Int32List: (int length) => malloc<Int32>(length).asTypedList(length), | |
| Uint32List: (int length) => malloc<Uint32>(length).asTypedList(length), | |
| Float32List: (int length) => malloc<Float>(length).asTypedList(length), | |
| Float64List: (int length) => malloc<Double>(length).asTypedList(length), | |
| }; | |
| static final Map<Type, TypedDataViewCreator> _viewCreatorRegistry = { | |
| Int8List: (ByteBuffer b, int o, int l) => b.asInt8List(o, l), | |
| Uint8List: (ByteBuffer b, int o, int l) => b.asUint8List(o, l), | |
| Int16List: (ByteBuffer b, int o, int l) => b.asInt16List(o, l), | |
| Uint16List: (ByteBuffer b, int o, int l) => b.asUint16List(o, l), | |
| Int32List: (ByteBuffer b, int o, int l) => b.asInt32List(o, l), | |
| Uint32List: (ByteBuffer b, int o, int l) => b.asUint32List(o, l), | |
| Float32List: (ByteBuffer b, int o, int l) => b.asFloat32List(o, l), | |
| Float64List: (ByteBuffer b, int o, int l) => b.asFloat64List(o, l), | |
| }; | |
| /// Provides a managed scope for using a buffer pool instance. | |
| static void usingPool<T extends TypedData>({ | |
| PoolConfig? config, | |
| bool offHeap = false, | |
| required void Function(InternalBufferPool<T>) fn, | |
| }) { | |
| final creator = | |
| (offHeap ? _heapCreatorRegistry[T] : _creatorRegistry[T]) | |
| as TypedDataCreator<T>?; | |
| final viewCreator = _viewCreatorRegistry[T] as TypedDataViewCreator<T>?; | |
| if (creator == null || viewCreator == null) { | |
| throw Exception( | |
| 'Unsupported TypedArray type: $T. ' | |
| 'Please ensure it is one of the standard `TypedData` types.', | |
| ); | |
| } | |
| final finalConfig = config ?? const PoolConfig(); | |
| final bufferPool = InternalBufferPool<T>(finalConfig, creator, viewCreator); | |
| fn(bufferPool); | |
| } | |
| } | |
| // --- Benchmark Configuration --- | |
| @pragma('vm:prefer-inline') | |
| void touchEveryElement(TypedData buffer) { | |
| // unique bit-pattern for each type | |
| if (buffer is Uint8List) { | |
| var sum = 0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = (i & 0xFF); // 0 … 255 | |
| sum += buffer[i]; | |
| } | |
| // prevent dead-code elimination | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Int8List) { | |
| var sum = 0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = ((i & 0x7F) - 64); // -64 … 63 | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Uint16List) { | |
| var sum = 0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = (i & 0xFFFF); // 0 … 65 535 | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Int16List) { | |
| var sum = 0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = ((i & 0x7FFF) - 16000); // -16000 … 32 767 | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Uint32List) { | |
| var sum = 0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = (0xAAAAAAAA + i); // big unsigned | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Int32List) { | |
| var sum = 0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = (0x7FFFFFF0 - i); // big positive | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Float32List) { | |
| var sum = 0.0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = (i * 1.234).toDouble(); // float pattern | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } else if (buffer is Float64List) { | |
| var sum = 0.0; | |
| for (var i = 0; i < buffer.length; ++i) { | |
| buffer[i] = (i * 3.141592653589793); // double pattern | |
| sum += buffer[i]; | |
| } | |
| if (sum == 0xDEADBEEF) print(sum); | |
| } | |
| } | |
| /// A generic function to run a full benchmark suite for a given TypedData type. | |
| Future<void> runBenchmarkForType<T extends TypedData>({ | |
| required String name, | |
| PoolConfig? config, | |
| bool offHeap = false, | |
| }) async { | |
| final random = Random(); | |
| const warmupOps = 100; | |
| const benchmarkOps = 10000; | |
| final bufferSizes = [ | |
| 128, | |
| 256, | |
| 512, | |
| 1024, | |
| 2048, | |
| 4096, | |
| 8192, | |
| 16384, | |
| 32768, | |
| 65536, | |
| 131072, | |
| ]; | |
| final warmupIndices = List<int>.generate( | |
| warmupOps, | |
| (_) => bufferSizes[random.nextInt(bufferSizes.length)], | |
| ); | |
| final benchmarkIndices = List<int>.generate( | |
| benchmarkOps, | |
| (_) => bufferSizes[random.nextInt(bufferSizes.length)], | |
| ); | |
| print('\n============================================================'); | |
| print('PERFORMANCE BENCHMARK FOR: $name'); | |
| print('============================================================'); | |
| print('Configuration:'); | |
| print(' - Warm-up Operations: $warmupOps'); | |
| print(' - Benchmark Operations: $benchmarkOps'); | |
| print(' - Buffer Sizes: $bufferSizes'); | |
| print( | |
| ' - Pool Capacity: ${config?.capacity.name ?? "Initial"}\n', | |
| ); | |
| BufferPool.usingPool<T>( | |
| config: config, | |
| offHeap: offHeap, | |
| fn: (pool) { | |
| // Warm-up | |
| print('PHASE 1: WARM-UP'); | |
| for (final size in warmupIndices) { | |
| final buffer = pool.acquire(size); | |
| pool.release(buffer); | |
| } | |
| print('Warm-up complete.\n'); | |
| void measure(String label, void Function() fn) { | |
| final sw = Stopwatch()..start(); | |
| fn(); | |
| sw.stop(); | |
| final timeMs = sw.elapsedMilliseconds; | |
| final opsPerSec = timeMs > 0 | |
| ? (benchmarkOps * 1000 ~/ timeMs) | |
| : benchmarkOps * 1000; | |
| print( | |
| 'Test: $label | Time: ${timeMs.toString().padLeft(6)} ms | ' | |
| 'Ops/sec: ~${opsPerSec.toString().padLeft(10)}', | |
| ); | |
| } | |
| measure("Pooled acquire/release", () { | |
| for (final size in benchmarkIndices) { | |
| final buffer = pool.acquire(size); | |
| touchEveryElement(buffer.data); | |
| pool.release(buffer); | |
| } | |
| }); | |
| measure("Pooled safe acquire/release", () { | |
| for (final size in benchmarkIndices) { | |
| final buffer = pool.acquireSafe(size); | |
| touchEveryElement(buffer.data); | |
| pool.release(buffer); | |
| } | |
| }); | |
| measure("Control (Non-Pooled)", () { | |
| for (final size in benchmarkIndices) { | |
| T buffer; | |
| if (T == Uint8List) { | |
| buffer = Uint8List(size) as T; | |
| } else if (T == Uint16List) { | |
| buffer = Uint16List(size) as T; | |
| } else if (T == Int32List) { | |
| buffer = Int32List(size) as T; | |
| } else if (T == Float32List) { | |
| buffer = Float32List(size) as T; | |
| } else if (T == Float64List) { | |
| buffer = Float64List(size) as T; | |
| } else { | |
| throw UnsupportedError("Type not supported"); | |
| } | |
| touchEveryElement(buffer); | |
| } | |
| }); | |
| }, | |
| ); | |
| print('============================================================\n'); | |
| } | |
| void main() { | |
| final config = PoolConfig( | |
| minSize: 128, | |
| maxSize: 131072, | |
| capacity: Capacity.Full, | |
| ); | |
| runBenchmarkForType<Uint8List>(name: 'Uint8List', config: config); | |
| runBenchmarkForType<Uint8List>( | |
| name: 'Uint8List Off-Heap', | |
| config: config, | |
| offHeap: true, | |
| ); | |
| runBenchmarkForType<Uint16List>(name: 'Uint16List', config: config); | |
| runBenchmarkForType<Uint16List>( | |
| name: 'Uint16List Off-Heap', | |
| config: config, | |
| offHeap: true, | |
| ); | |
| runBenchmarkForType<Int32List>(name: 'Int32List', config: config); | |
| runBenchmarkForType<Int32List>( | |
| name: 'Int32List Off-Heap', | |
| config: config, | |
| offHeap: true, | |
| ); | |
| runBenchmarkForType<Float32List>(name: 'Float32List', config: config); | |
| runBenchmarkForType<Float32List>( | |
| name: 'Float32List Off-Heap', | |
| config: config, | |
| offHeap: true, | |
| ); | |
| runBenchmarkForType<Float64List>(name: 'Float64List', config: config); | |
| runBenchmarkForType<Float64List>( | |
| name: 'Float64List Off-Heap', | |
| config: config, | |
| offHeap: true, | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @file A high-performance buffer pool implementation in JavaScript for managing TypedArrays, with examples | |
| * @author Piyush Katariya | |
| * @version 1.3.0 | |
| * @description Works in both Browser and Node.js environments | |
| */ | |
| /** | |
| * An enum defining fixed allocation capacities for buffer pool segments. | |
| */ | |
| const Capacity = Object.freeze({ | |
| Initial: 1, | |
| Quarter: 16, | |
| Half: 32, | |
| Distinct: 48, | |
| Full: 64 | |
| }); | |
| /** | |
| * Configuration class for buffer pool initialization. | |
| */ | |
| class PoolConfig { | |
| constructor({ | |
| minSize = 16, | |
| maxSize = 256 * 1024, | |
| capacity = Capacity.Initial, | |
| bucketRange = null, | |
| segmentsPerBucket = null, | |
| buffersPerSegment = null, | |
| enableAdaptiveGrowth = true | |
| } = {}) { | |
| this.minSize = minSize; | |
| this.maxSize = maxSize; | |
| this.capacity = capacity; | |
| this.bucketRange = bucketRange; | |
| this.segmentsPerBucket = segmentsPerBucket; | |
| this.buffersPerSegment = buffersPerSegment; | |
| this.enableAdaptiveGrowth = enableAdaptiveGrowth; | |
| } | |
| /** | |
| * Validates the configuration parameters. | |
| */ | |
| validate() { | |
| if (this.minSize < 16) throw new Error('minSize cannot be less than 16.'); | |
| if (this.maxSize > 16 * 1024 * 1024) { | |
| throw new Error('maxSize cannot be greater than 16 MB.'); | |
| } | |
| if (this.minSize >= this.maxSize) throw new Error('minSize must be less than maxSize.'); | |
| if (this.bucketRange !== null || this.segmentsPerBucket !== null || this.buffersPerSegment !== null) { | |
| if (this.bucketRange === null || this.segmentsPerBucket === null || this.buffersPerSegment === null) { | |
| throw new Error('All custom slab configuration arrays must be provided together.'); | |
| } | |
| if (this.bucketRange.length !== this.segmentsPerBucket.length || | |
| this.bucketRange.length !== this.buffersPerSegment.length) { | |
| throw new Error('All arrays in custom slab config must have the same length.'); | |
| } | |
| if (this.bucketRange.length > BufferPool.maxSlabs) { | |
| throw new Error( | |
| `The length of arrays in custom slab config cannot exceed the maximum of ${BufferPool.maxSlabs}.`); | |
| } | |
| for (const bucket of this.bucketRange) { | |
| if (bucket <= 0) { | |
| throw new Error('All bucket sizes in custom slab config must be positive numbers.'); | |
| } | |
| } | |
| for (let i = 0; i < this.bucketRange.length - 1; i++) { | |
| if (this.bucketRange[i] >= this.bucketRange[i + 1]) { | |
| throw new Error( | |
| 'bucketRange in custom slab config must be sorted in ascending order and contain no duplicate values.'); | |
| } | |
| } | |
| for (const segmentCount of this.segmentsPerBucket) { | |
| if (segmentCount > BufferPool.maxSegments || segmentCount <= 0) { | |
| throw new Error( | |
| `The number of segments per bucket in custom slab config must be between 1 and ${BufferPool.maxSegments}.`); | |
| } | |
| } | |
| for (const buffersCount of this.buffersPerSegment) { | |
| if (buffersCount > BufferPool.maxBuffers || buffersCount <= 0) { | |
| throw new Error( | |
| `The number of buffers per segment in custom slab config must be between 1 and ${BufferPool.maxBuffers}.`); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Abstract base class for buffers managed by the pool. | |
| */ | |
| class IBuffer { | |
| constructor() { | |
| this.data = null; | |
| this.meta = 0; | |
| this.isPooled = false; | |
| } | |
| } | |
| /** | |
| * Represents a memory buffer that is a view into a larger slab segment. | |
| */ | |
| class PooledBuffer extends IBuffer { | |
| // Adjusted masks: slab(4) + segment(6) + buffer(5) = 15 bits | |
| static _slabMask = 0x7800; // 0111100000000000 | |
| static _segmentMask = 0x07E0; // 0000011111100000 | |
| static _bufferMask = 0x001F; // 0000000000011111 | |
| static _slabShift = 11; | |
| static _segmentShift = 5; | |
| constructor(slabIndex, segmentIndex, bufferIndex, elementCount, segmentData, viewCreator) { | |
| super(); | |
| this.meta = (slabIndex << PooledBuffer._slabShift) | | |
| (segmentIndex << PooledBuffer._segmentShift) | | |
| bufferIndex; | |
| const bytesPerElement = segmentData.BYTES_PER_ELEMENT; | |
| const byteOffset = bufferIndex * elementCount * bytesPerElement; | |
| this.data = viewCreator(segmentData.buffer, byteOffset, elementCount); | |
| this.isPooled = true; | |
| } | |
| get slabIndex() { | |
| return (this.meta & PooledBuffer._slabMask) >> PooledBuffer._slabShift; | |
| } | |
| get segmentIndex() { | |
| return (this.meta & PooledBuffer._segmentMask) >> PooledBuffer._segmentShift; | |
| } | |
| get bufferIndex() { | |
| return this.meta & PooledBuffer._bufferMask; | |
| } | |
| } | |
| /** | |
| * Represents a buffer that is not managed by the buffer pool. | |
| */ | |
| class NonPooledBuffer extends IBuffer { | |
| constructor(elementCount, creator) { | |
| super(); | |
| this.data = creator(elementCount); | |
| this.meta = 0; | |
| this.isPooled = false; | |
| } | |
| } | |
| /** | |
| * A Segment is a contiguous block of memory carved into fixed-size buffers. | |
| */ | |
| class Segment { | |
| constructor(bucketElementCount, slabIndex, segmentIndex, maxBuffers, creator, viewCreator) { | |
| this.bucketElementCount = bucketElementCount; | |
| this.slabIndex = slabIndex; | |
| this.segmentIndex = segmentIndex; | |
| this.maxBuffers = maxBuffers; | |
| this._creator = creator; | |
| this._viewCreator = viewCreator; | |
| this.nextFreeBufferIndex = 0; | |
| this.reset(); | |
| } | |
| reset() { | |
| const totalElements = this.bucketElementCount * this.maxBuffers; | |
| this.data = this._creator(totalElements); | |
| this.localPooledBuffers = new Array(this.maxBuffers).fill(null); | |
| this._initializeSegment(); | |
| } | |
| _initializeSegment() { | |
| for (let i = 0; i < this.maxBuffers; i++) { | |
| const buffer = new PooledBuffer( | |
| this.slabIndex, this.segmentIndex, i, this.bucketElementCount, | |
| this.data, this._viewCreator); | |
| this.localPooledBuffers[i] = buffer; | |
| } | |
| this.nextFreeBufferIndex = this.maxBuffers - 1; | |
| } | |
| allocate() { | |
| if (this.nextFreeBufferIndex >= 0) { | |
| const buffer = this.localPooledBuffers[this.nextFreeBufferIndex]; | |
| this.localPooledBuffers[this.nextFreeBufferIndex] = null; | |
| this.nextFreeBufferIndex--; | |
| return buffer; | |
| } | |
| return new NonPooledBuffer(this.bucketElementCount, this._creator); | |
| } | |
| deallocate(buffer) { | |
| if (buffer instanceof PooledBuffer) { | |
| if (this.nextFreeBufferIndex < this.maxBuffers - 1) { | |
| this.nextFreeBufferIndex++; | |
| this.localPooledBuffers[this.nextFreeBufferIndex] = buffer; | |
| } else { | |
| console.warn("WARN: Attempting to deallocate a buffer to a full segment. This should not happen."); | |
| } | |
| } | |
| } | |
| get freeCount() { | |
| return this.nextFreeBufferIndex + 1; | |
| } | |
| } | |
| /** | |
| * A Slab is a fixed-size buffer pool that manages up to 64 segments. | |
| */ | |
| class Slab { | |
| constructor(bucketElementCount, slabIndex, initialCapacity, buffersPerSegment, creator, viewCreator) { | |
| this.bucketElementCount = bucketElementCount; | |
| this.slabIndex = slabIndex; | |
| this.initialCapacity = initialCapacity; | |
| this.buffersPerSegment = buffersPerSegment; | |
| this._creator = creator; | |
| this._viewCreator = viewCreator; | |
| this.segments = []; | |
| this.freeSegmentIndices = []; | |
| this._allocationAttempts = 0; | |
| this.reset(); | |
| } | |
| reset() { | |
| this.segments.length = 0; | |
| this.freeSegmentIndices.length = 0; | |
| for (let i = 0; i < this.initialCapacity; i++) { | |
| this.addSegment(); | |
| } | |
| } | |
| addSegment() { | |
| if (this.segments.length >= BufferPool.maxSegments) { | |
| throw new Error('Maximum number of segments reached for this slab.'); | |
| } | |
| const newSegment = new Segment( | |
| this.bucketElementCount, | |
| this.slabIndex, | |
| this.segments.length, | |
| this.buffersPerSegment, | |
| this._creator, | |
| this._viewCreator | |
| ); | |
| this.segments.push(newSegment); | |
| this.freeSegmentIndices.push(this.segments.length - 1); | |
| } | |
| allocate() { | |
| let segmentIndex = null; | |
| if (this.freeSegmentIndices.length > 0) { | |
| segmentIndex = this.freeSegmentIndices.pop(); | |
| } | |
| if (segmentIndex === null) { | |
| if (this.segments.length < BufferPool.maxSegments) { | |
| this.addSegment(); | |
| segmentIndex = this.segments.length - 1; | |
| this.freeSegmentIndices.pop(); | |
| } else { | |
| if (this._allocationAttempts % 1000 === 0) { | |
| console.warn( | |
| `WARN: All segments in slab for bucket element count ${this.bucketElementCount} are full. Returning unmanaged buffers.`); | |
| } | |
| this._allocationAttempts++; | |
| return new NonPooledBuffer(this.bucketElementCount, this._creator); | |
| } | |
| } | |
| const segment = this.segments[segmentIndex]; | |
| const buffer = segment.allocate(); | |
| if (segment.freeCount > 0) { | |
| this.freeSegmentIndices.push(segmentIndex); | |
| } | |
| return buffer; | |
| } | |
| deallocate(buffer) { | |
| if (!(buffer instanceof PooledBuffer)) return; | |
| if (buffer.slabIndex !== this.slabIndex) { | |
| console.error( | |
| `ERROR: Deallocation Error: Buffer's slab index (${buffer.slabIndex}) does not match current slab's index (${this.slabIndex}).`); | |
| return; | |
| } | |
| const segmentIndex = buffer.segmentIndex; | |
| if (segmentIndex >= 0 && segmentIndex < this.segments.length) { | |
| const wasFull = this.segments[segmentIndex].freeCount === 0; | |
| this.segments[segmentIndex].deallocate(buffer); | |
| if (wasFull) { | |
| this.freeSegmentIndices.push(segmentIndex); | |
| } | |
| } else { | |
| console.error( | |
| `ERROR: Deallocation Error: Buffer's segment index (${segmentIndex}) is out of bounds for this slab.`); | |
| } | |
| } | |
| get freeCount() { | |
| return this.segments.reduce((total, segment) => total + segment.freeCount, 0); | |
| } | |
| } | |
| /** | |
| * Manages multiple slabs, providing a unified interface for allocation and deallocation. | |
| */ | |
| class InternalBufferPool { | |
| constructor(config, creator, viewCreator) { | |
| config.validate(); | |
| this.slabs = []; | |
| this.buckets = []; | |
| this.bucketMap = {}; | |
| this._creator = creator; | |
| this._viewCreator = viewCreator; | |
| this._fallbackWarningCount = 0; | |
| const initialCapacity = config.capacity; | |
| let finalBucketRange, finalSegmentsPerBucket, finalBuffersPerSegment; | |
| if (config.bucketRange !== null) { | |
| finalBucketRange = config.bucketRange; | |
| finalSegmentsPerBucket = config.segmentsPerBucket; | |
| finalBuffersPerSegment = config.buffersPerSegment; | |
| } else { | |
| finalBucketRange = InternalBufferPool._generateBetterBuckets(config.minSize, config.maxSize); | |
| finalSegmentsPerBucket = new Array(finalBucketRange.length).fill(initialCapacity); | |
| finalBuffersPerSegment = new Array(finalBucketRange.length).fill(BufferPool.maxBuffers); | |
| } | |
| this.buckets = finalBucketRange; | |
| for (let i = 0; i < this.buckets.length; i++) { | |
| const bucket = this.buckets[i]; | |
| const segments = finalSegmentsPerBucket[i]; | |
| const buffers = finalBuffersPerSegment[i]; | |
| const slab = new Slab(bucket, i, segments, buffers, creator, viewCreator); | |
| this.slabs.push(slab); | |
| this.bucketMap[bucket] = i; | |
| } | |
| } | |
| static _generateBetterBuckets(minSize, maxSize) { | |
| const sizes = new Set(); | |
| // Add power-of-2 sizes and their multiples | |
| for (let power = 4; power <= 20; power++) { | |
| const base = 1 << power; // 2^power | |
| if (base >= minSize && base <= maxSize) { | |
| sizes.add(base); | |
| // Add 1.5x multiples for better coverage | |
| const mult = Math.round(base * 1.5); | |
| if (mult <= maxSize) sizes.add(mult); | |
| } | |
| } | |
| // Add common small sizes | |
| for (let size = minSize; size <= Math.min(128, maxSize); size += 8) { | |
| sizes.add(size); | |
| } | |
| // Ensure we have the min and max | |
| sizes.add(minSize); | |
| sizes.add(maxSize); | |
| const result = Array.from(sizes).sort((a, b) => a - b); | |
| // Limit to maxSlabs (16) | |
| if (result.length > BufferPool.maxSlabs) { | |
| // Keep evenly distributed sizes | |
| const step = result.length / BufferPool.maxSlabs; | |
| const filtered = []; | |
| for (let i = 0; i < BufferPool.maxSlabs; i++) { | |
| filtered.push(result[Math.floor(i * step)]); | |
| } | |
| return filtered; | |
| } | |
| return result; | |
| } | |
| reset() { | |
| for (const slab of this.slabs) { | |
| slab.reset(); | |
| } | |
| } | |
| acquire(requestedElementCount) { | |
| let low = 0; | |
| let high = this.buckets.length - 1; | |
| let slabIndex = -1; | |
| while (low <= high) { | |
| const mid = Math.floor(low + (high - low) / 2); | |
| const currentBucket = this.buckets[mid]; | |
| if (currentBucket >= requestedElementCount) { | |
| slabIndex = mid; | |
| high = mid - 1; | |
| } else { | |
| low = mid + 1; | |
| } | |
| } | |
| if (slabIndex !== -1) { | |
| const slab = this.slabs[slabIndex]; | |
| return slab.allocate(); | |
| } | |
| if (this._fallbackWarningCount < 5) { | |
| console.warn( | |
| `WARN: No suitable slab found for requested count of ${requestedElementCount}. Returning a new, unmanaged buffer.`); | |
| this._fallbackWarningCount++; | |
| if (this._fallbackWarningCount === 5) { | |
| console.warn('WARN: Suppressing further fallback warnings...'); | |
| } | |
| } | |
| return new NonPooledBuffer(requestedElementCount, this._creator); | |
| } | |
| acquireSafe(requestedElementCount) { | |
| const buffer = this.acquire(requestedElementCount); | |
| if (buffer.isPooled) { | |
| // Clear the entire buffer data | |
| const view = new Uint8Array(buffer.data.buffer, buffer.data.byteOffset, buffer.data.byteLength); | |
| view.fill(0); | |
| } | |
| return buffer; | |
| } | |
| release(buffer) { | |
| if (buffer.isPooled) { | |
| const pooledBuffer = buffer; | |
| const slabIndex = pooledBuffer.slabIndex; | |
| if (slabIndex >= 0 && slabIndex < this.slabs.length) { | |
| const slab = this.slabs[slabIndex]; | |
| slab.deallocate(pooledBuffer); | |
| } else { | |
| console.error('ERROR: No matching slab found for buffer. Cannot deallocate.'); | |
| } | |
| } | |
| // Silently ignore unmanaged buffers | |
| } | |
| get freeCount() { | |
| return this.slabs.reduce((total, slab) => total + slab.freeCount, 0); | |
| } | |
| } | |
| /** | |
| * The main public-facing class for the buffer pool. | |
| */ | |
| class BufferPool { | |
| static maxSlabs = 16; | |
| static maxSegments = 64; | |
| static maxBuffers = 32; | |
| static _creatorRegistry = { | |
| Int8Array: (length) => new Int8Array(length), | |
| Uint8Array: (length) => new Uint8Array(length), | |
| Int16Array: (length) => new Int16Array(length), | |
| Uint16Array: (length) => new Uint16Array(length), | |
| Int32Array: (length) => new Int32Array(length), | |
| Uint32Array: (length) => new Uint32Array(length), | |
| Float32Array: (length) => new Float32Array(length), | |
| Float64Array: (length) => new Float64Array(length), | |
| }; | |
| static _viewCreatorRegistry = { | |
| Int8Array: (buffer, offset, length) => new Int8Array(buffer, offset, length), | |
| Uint8Array: (buffer, offset, length) => new Uint8Array(buffer, offset, length), | |
| Int16Array: (buffer, offset, length) => new Int16Array(buffer, offset, length), | |
| Uint16Array: (buffer, offset, length) => new Uint16Array(buffer, offset, length), | |
| Int32Array: (buffer, offset, length) => new Int32Array(buffer, offset, length), | |
| Uint32Array: (buffer, offset, length) => new Uint32Array(buffer, offset, length), | |
| Float32Array: (buffer, offset, length) => new Float32Array(buffer, offset, length), | |
| Float64Array: (buffer, offset, length) => new Float64Array(buffer, offset, length), | |
| }; | |
| /** | |
| * Provides a managed scope for using a buffer pool instance. | |
| * @param {string} arrayType - The TypedArray type ('Float32Array', 'Uint8Array', etc.) | |
| * @param {PoolConfig} config - Pool configuration | |
| * @param {Function} fn - Function to execute with the pool | |
| */ | |
| static usingPool(arrayType, config = null, fn) { | |
| if (typeof config === 'function') { | |
| fn = config; | |
| config = null; | |
| } | |
| const creator = this._creatorRegistry[arrayType]; | |
| const viewCreator = this._viewCreatorRegistry[arrayType]; | |
| if (!creator || !viewCreator) { | |
| throw new Error(`Unsupported TypedArray type: ${arrayType}. ` + | |
| 'Please ensure it is one of the standard TypedArray types.'); | |
| } | |
| const finalConfig = config || new PoolConfig(); | |
| const bufferPool = new InternalBufferPool(finalConfig, creator, viewCreator); | |
| return fn(bufferPool); | |
| } | |
| } | |
| // --- Examples --- | |
| function runLoadTestExample() { | |
| console.log("\n--- Running Load Test Example ---"); | |
| const numAllocations = 20000; | |
| const minSize = 16; | |
| const maxSize = 100; | |
| BufferPool.usingPool('Float32Array', new PoolConfig({ minSize, maxSize, capacity: Capacity.Full }), (pool) => { | |
| let unmanagedCount = 0; | |
| let pooledCount = 0; | |
| const allocatedBuffers = []; | |
| console.log(`Starting ${numAllocations} allocations and deallocations...`); | |
| // Stage 1: Allocate a large number of buffers of random sizes. | |
| for (let i = 0; i < numAllocations; i++) { | |
| const bufferSize = minSize + Math.floor(Math.random() * (maxSize - minSize + 1)); | |
| const buffer = pool.acquire(bufferSize); | |
| allocatedBuffers.push(buffer); | |
| if (buffer.isPooled) { | |
| pooledCount++; | |
| } else { | |
| unmanagedCount++; | |
| } | |
| } | |
| console.log(`\nInitial allocations complete. Total buffers acquired: ${allocatedBuffers.length}`); | |
| console.log(`Pooled buffers: ${pooledCount}`); | |
| console.log(`Unmanaged buffers: ${unmanagedCount}`); | |
| console.log(`Current free buffers in pool: ${pool.freeCount}`); | |
| // Stage 2: Deallocate all the buffers. | |
| console.log("\nReleasing all allocated buffers back to the pool..."); | |
| for (const buffer of allocatedBuffers) { | |
| pool.release(buffer); | |
| } | |
| console.log(`Final free buffers in pool: ${pool.freeCount}`); | |
| console.log("Deallocation complete. The pool should now be mostly full again."); | |
| }); | |
| } | |
| function runEfficiencyExample() { | |
| console.log("\n\n--- Running Sequential vs. Interleaved Efficiency Example ---"); | |
| // Scenario 1: Sequential Allocation and Deallocation | |
| console.log("\nScenario 1: Acquire all buffers, then release all buffers."); | |
| BufferPool.usingPool('Uint8Array', new PoolConfig({ minSize: 16, maxSize: 16000, capacity: Capacity.Initial }), (pool) => { | |
| const buffers = []; | |
| console.log(`Initial free count: ${pool.freeCount}`); | |
| // Acquire all | |
| for (let i = 0; i < 50; i++) { | |
| buffers.push(pool.acquire(20)); | |
| } | |
| console.log(`After acquiring 50 buffers: free count is ${pool.freeCount}`); | |
| // Release all | |
| for (const buffer of buffers) { | |
| pool.release(buffer); | |
| } | |
| console.log(`After releasing 50 buffers: free count is ${pool.freeCount}`); | |
| }); | |
| // Scenario 2: Interleaved Allocation and Deallocation | |
| console.log("\nScenario 2: Acquire and release buffers one by one."); | |
| BufferPool.usingPool('Uint8Array', new PoolConfig({ minSize: 16, maxSize: 16000, capacity: Capacity.Initial }), (pool) => { | |
| console.log(`Initial free count: ${pool.freeCount}`); | |
| // Acquire and release in a loop | |
| for (let i = 0; i < 50; i++) { | |
| const buffer = pool.acquire(20); | |
| pool.release(buffer); | |
| } | |
| console.log(`After 50 acquire/release cycles: free count is ${pool.freeCount}`); | |
| console.log("Notice how the free count remains stable, demonstrating memory reuse."); | |
| }); | |
| } | |
| function runMixedTypeExample() { | |
| console.log("\n\n--- Running Mixed Type Example ---"); | |
| console.log("\nUsing pool with Float32Array..."); | |
| BufferPool.usingPool('Float32Array', (pool) => { | |
| const buffer1 = pool.acquire(32); | |
| console.log(`Acquired a Float32Array of length ${buffer1.data.length}`); | |
| pool.release(buffer1); | |
| console.log(`Released Float32Array. Free count: ${pool.freeCount}`); | |
| }); | |
| console.log("\nUsing pool with Int16Array..."); | |
| BufferPool.usingPool('Int16Array', (pool) => { | |
| const buffer2 = pool.acquire(64); | |
| console.log(`Acquired an Int16Array of length ${buffer2.data.length}`); | |
| pool.release(buffer2); | |
| console.log(`Released Int16Array. Free count: ${pool.freeCount}`); | |
| }); | |
| } | |
| function runSlabExhaustionExample() { | |
| console.log("\n\n--- Running Slab Exhaustion Example ---"); | |
| const bufferSize = 20; | |
| const initialBufferCount = BufferPool.maxBuffers; | |
| BufferPool.usingPool('Int32Array', new PoolConfig({ minSize: 16, maxSize: 100, capacity: Capacity.Initial }), (pool) => { | |
| const allocated = []; | |
| console.log(`Initial free buffers for size ${bufferSize}: ${pool.freeCount}`); | |
| console.log(`\nAllocating ${initialBufferCount} buffers to exhaust the initial segment...`); | |
| for (let i = 0; i < initialBufferCount; i++) { | |
| allocated.push(pool.acquire(bufferSize)); | |
| } | |
| console.log(`Free count after exhausting initial segment: ${pool.freeCount}`); | |
| console.log("\nAllocating one more buffer to force a new segment to be created..."); | |
| const extraBuffer = pool.acquire(bufferSize); | |
| allocated.push(extraBuffer); | |
| console.log(`Free count after allocating a new segment: ${pool.freeCount}`); | |
| console.log("A new segment was added to the slab for this buffer size."); | |
| // Clean up | |
| for (const buffer of allocated) { | |
| pool.release(buffer); | |
| } | |
| console.log(`\nFinal free count after releasing all buffers: ${pool.freeCount}`); | |
| }); | |
| } | |
| function runSafeAcquireExample() { | |
| console.log("\n\n--- Running Safe Acquire Example ---"); | |
| BufferPool.usingPool('Uint8Array', (pool) => { | |
| console.log("Acquiring a buffer and filling it with data..."); | |
| const buffer1 = pool.acquire(16); | |
| if (buffer1 instanceof PooledBuffer) { | |
| buffer1.data.fill(99, 0, 16); | |
| console.log(`Buffer 1 data (first 4 elements): [${Array.from(buffer1.data.subarray(0, 4))}]`); | |
| } | |
| pool.release(buffer1); | |
| console.log("Buffer 1 released."); | |
| console.log("\nAcquiring a buffer of the same size with `acquire`..."); | |
| const buffer2 = pool.acquire(16); | |
| if (buffer2 instanceof PooledBuffer) { | |
| console.log(`Buffer 2 data (first 4 elements): [${Array.from(buffer2.data.subarray(0, 4))}]`); | |
| console.log("Note: The data from the previous buffer is still present."); | |
| } | |
| pool.release(buffer2); | |
| console.log("\nAcquiring a buffer of the same size with `acquireSafe`..."); | |
| const buffer3 = pool.acquireSafe(16); | |
| if (buffer3 instanceof PooledBuffer) { | |
| console.log(`Buffer 3 data (first 4 elements): [${Array.from(buffer3.data.subarray(0, 4))}]`); | |
| console.log("Note: The data has been zeroed out for security."); | |
| } | |
| pool.release(buffer3); | |
| }); | |
| } | |
| // Main execution function | |
| function main() { | |
| runLoadTestExample(); | |
| runEfficiencyExample(); | |
| runMixedTypeExample(); | |
| runSlabExhaustionExample(); | |
| runSafeAcquireExample(); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment