From 89e3859de3a70f02784c6bf93ff6b8c80d410c68 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 24 May 2026 19:02:07 +0300 Subject: [PATCH 01/10] separate files --- lib/src/binary_writer.dart | 373 +------------------------------- lib/src/binary_writer_pool.dart | 279 ++++++++++++++++++++++++ lib/src/string_utils.dart | 95 ++++++++ 3 files changed, 377 insertions(+), 370 deletions(-) create mode 100644 lib/src/binary_writer_pool.dart create mode 100644 lib/src/string_utils.dart diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index fa2564d..1327b0b 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -4,6 +4,9 @@ import 'dart:typed_data'; // for explanation of max safe integer in JavaScript. import 'constants_native.dart' if (dart.library.js_util) 'constants_web.dart'; +part 'binary_writer_pool.dart'; +part 'string_utils.dart'; + /// A high-performance binary writer for encoding data into a byte buffer. /// /// Provides methods for writing various data types including: @@ -882,374 +885,4 @@ final class _WriterState { } } -/// Calculates the UTF-8 byte length of the given string without encoding it. -/// -/// This function efficiently computes the number of bytes required to -/// encode the string in UTF-8, taking into account multi-byte characters -/// and surrogate pairs. It's optimized with an ASCII fast path that processes -/// up to 4 ASCII characters at once. -/// -/// Useful for: -/// - Pre-allocating buffers of the correct size -/// - Calculating message sizes before serialization -/// - Validating string length constraints -/// -/// Performance: -/// - ASCII strings: ~4 bytes per loop iteration -/// - Mixed content: Falls back to character-by-character analysis -/// -/// Example: -/// ```dart -/// final text = 'Hello, δΈ–η•Œ! 🌍'; -/// final byteLength = getUtf8Length(text); // 20 bytes -/// // vs text.length would be 15 characters -/// ``` -/// -/// @param s The input string. -/// @return The number of bytes needed for UTF-8 encoding. -int getUtf8Length(String value) { - if (value.isEmpty) { - return 0; - } - - final len = value.length; - var bytes = 0; - var i = 0; - - while (i < len) { - final char = value.codeUnitAt(i); - - // ASCII fast path - if (char < 0x80) { - // Process 4 ASCII characters at a time - final end = len - 4; - while (i <= end) { - final mask = - value.codeUnitAt(i) | - value.codeUnitAt(i + 1) | - value.codeUnitAt(i + 2) | - value.codeUnitAt(i + 3); - - if (mask >= 0x80) { - break; - } - - i += 4; - bytes += 4; - } - - // Handle remaining ASCII characters - while (i < len && value.codeUnitAt(i) < 0x80) { - i++; - bytes++; - } - if (i >= len) { - return bytes; - } - continue; - } - - // 2-byte sequence - if (char < 0x800) { - bytes += 2; - i++; - } - // 3-byte sequence - else if (char >= 0xD800 && char <= 0xDBFF && i + 1 < len) { - final next = value.codeUnitAt(i + 1); - if (next >= 0xDC00 && next <= 0xDFFF) { - bytes += 4; - i += 2; - continue; - } - // Malformed surrogate pair - bytes += 3; - i++; - } - // 3-byte sequence - else { - bytes += 3; - i++; - } - } - - return bytes; -} - -// Disable lint to allow static-only class for pooling -/// Object pool for reusing [BinaryWriter] instances to reduce GC pressure. -/// -/// This pool maintains a cache of [BinaryWriter] instances with their -/// internal buffers, allowing efficient reuse without allocating new memory -/// for each write operation. -/// -/// ## Features -/// - **Automatic reuse:** [acquire] gets a pooled writer or creates a new one -/// - **Memory bounds:** Only reuses writers with buffers ≀ 64 KiB -/// - **Size limits:** Maintains max 32 pooled instances -/// - **Safe:** Prevents double-release and handles edge cases -/// -/// ## Usage Pattern -/// Use `acquire()` and `release()` for short-lived write operations: -/// -/// ```dart -/// final writer = BinaryWriterPool.acquire(); -/// try { -/// writer.writeUint32(42); -/// writer.writeString('Hello'); -/// final bytes = writer.toBytes(); -/// // Use bytes... -/// } finally { -/// BinaryWriterPool.release(writer); // Return to pool -/// } -/// ``` -/// -/// ## Thread Safety -/// This pool is isolate-local. Each Dart isolate maintains its own -/// static pool instance. -/// -/// Avoid sharing [BinaryWriter] instances between different isolates. -/// For concurrent operations within the same isolate, ensure writers -/// are acquired and released synchronously or protected by logic -/// to prevent interleaved usage. -/// -/// ## Performance Considerations -/// - Pooling is beneficial for high-frequency write operations -/// - Overhead is minimal for single-use writers (use regular constructor) -/// - Large buffers (>64 KiB) are discarded to avoid memory waste -/// -/// ## Memory Management -/// - Pool max size: 32 writers -/// - Max reusable buffer: 64 KiB -/// - Default buffer size: 1 KiB -/// - Use [clear] to free pooled memory explicitly -/// -/// See also: [BinaryWriter], [stats] for pool monitoring -// ignore: avoid_classes_with_only_static_members -abstract final class BinaryWriterPool { - // The internal pool of reusable writer states. - static final _pool = <_WriterState>[]; - - /// Maximum number of writers to keep in the pool. - static const _maxPoolSize = 32; - - /// Default initial buffer size for new writers (1 KiB). - static const _defaultBufferSize = 1024; - - /// Maximum buffer capacity allowed for pooling (64 KiB). - /// Writers that exceed this size are discarded to free up system memory - static const int _maxReusableCapacity = 64 * 1024; - - // Performance counters - static var _acquireHit = 0; - static var _acquireMiss = 0; - static var _peakPoolSize = 0; - static var _discardedLargeBuffers = 0; - - /// Acquires a [BinaryWriter] from the pool or creates a new one. - /// - /// Returns a pooled writer if available, otherwise creates a fresh instance - /// with the default buffer size (1 KiB). - /// - /// The returned writer is ready to use and should be returned to the pool - /// via [release] when no longer needed. - /// - /// **Best Practice:** Always use a `try-finally` block. - /// - /// There are two ways to get the data: - /// 1. Use [BinaryWriter.toBytes] if you consume data **inside** the try - /// block (zero-copy view). - /// 2. Use [BinaryWriter.takeBytes] if you need to **return** the data - /// (transfers buffer ownership). - /// - /// ```dart - /// final writer = BinaryWriterPool.acquire(); - /// try { - /// writer.writeUint32(123); - /// return writer.takeBytes(); - /// } finally { - /// BinaryWriterPool.release(writer); - /// } - /// ``` - /// - /// Returns: A [BinaryWriter] ready for use. - static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { - if (_pool.isNotEmpty) { - _acquireHit++; - final state = _pool.removeLast().._isInPool = false; - - if (state.capacity < defaultBufferSize) { - state.ensureSize(defaultBufferSize); - } - return BinaryWriter._(state); - } - - _acquireMiss++; - - return BinaryWriter(initialBufferSize: defaultBufferSize); - } - - /// Acquires a writer, executes the given [action], and automatically - /// releases the writer back to the pool. - /// - /// This is the recommended way to use the pool as it ensures the writer - /// is always released even if an exception occurs. - /// - /// Example: - /// ```dart - /// final bytes = BinaryWriterPool.withWriter((writer) { - /// writer.writeUint32(42); - /// return writer.takeBytes(); - /// }); - /// ``` - static T withWriter( - T Function(BinaryWriter writer) action, [ - int defaultBufferSize = _defaultBufferSize, - ]) { - final writer = acquire(defaultBufferSize); - try { - return action(writer); - } finally { - release(writer); - } - } - - /// Returns a [BinaryWriter] to the pool for future reuse. - /// - /// The writer is reset (offset cleared) and stored for future [acquire] - /// calls. Writers with buffers larger than 64 KiB are not pooled to avoid - /// long-term memory retention. - /// - /// **Safe to call multiple times** (duplicate releases are ignored). - /// - /// Only writers with capacity ≀ 64 KiB are pooled. Writers exceeding this - /// limit are discarded, allowing the buffer to be garbage collected. - /// - /// **Do NOT use the writer after releasing it:** - /// - /// ```dart - /// final writer = BinaryWriterPool.acquire(); - /// writer.writeUint32(42); - /// final bytes = writer.toBytes(); - /// BinaryWriterPool.release(writer); - /// // DON'T USE writer here - it's returned to the pool and may be reused! - /// ``` - /// - /// Parameters: - /// - [writer]: The [BinaryWriter] to return to the pool - static void release(BinaryWriter writer) { - final state = writer._ws; - - // Prevent double-release and state corruption - if (state._isInPool) { - return; - } - - // Only pool writers with reasonable buffer sizes - // Prevents memory bloat from occasional large allocations - if (state.capacity <= _maxReusableCapacity && _pool.length < _maxPoolSize) { - state - ..offset = 0 - .._isInPool = true; - _pool.add(state); - - // Track peak pool size - if (_pool.length > _peakPoolSize) { - _peakPoolSize = _pool.length; - } - } else if (state.capacity > _maxReusableCapacity) { - _discardedLargeBuffers++; - } - } - - /// Returns pool statistics for monitoring and debugging. - /// - /// Useful for performance analysis and detecting pool inefficiencies. - /// - /// Returns a map with keys: - /// - `'pooled'`: Number of writers currently in the pool - /// - `'maxPoolSize'`: Maximum pool capacity - /// - `'defaultBufferSize'`: Initial buffer size for new writers - /// - `'maxReusableCapacity'`: Maximum buffer size for pooling - /// - `'acquireHit'`: Number of successful reuses from pool - /// - `'acquireMiss'`: Number of new writer allocations - /// - `'peakPoolSize'`: Maximum pool size reached - /// - `'discardedLargeBuffers'`: Number of oversized buffers discarded - /// - /// Example: - /// ```dart - /// final stats = BinaryWriterPool.stats; - /// print('Pooled writers: ${stats.pooled}'); // 5 - /// print('Hit rate: ${stats.acquireHit / (stats.acquireHit + stats.acquireMiss)}'); - /// ``` - static PoolStatistics get stats => PoolStatistics({ - 'pooled': _pool.length, - 'maxPoolSize': _maxPoolSize, - 'defaultBufferSize': _defaultBufferSize, - 'maxReusableCapacity': _maxReusableCapacity, - 'acquireHit': _acquireHit, - 'acquireMiss': _acquireMiss, - 'peakPoolSize': _peakPoolSize, - 'discardedLargeBuffers': _discardedLargeBuffers, - }); - - /// Clears the pool, releasing all cached writers. - /// - /// Use this to: - /// - Free memory during low-activity periods - /// - Reset pool state in tests - /// - Handle memory pressure - /// - /// After clearing, subsequent [acquire] calls will create new writers. - /// - /// Example: - /// ```dart - /// BinaryWriterPool.clear(); // All pooled writers discarded - /// ``` - static void clear() { - // Assist GC by breaking links to potentially large byte buffers - for (var i = 0; i < _pool.length; i++) { - _pool[i] - ..list = Uint8List(0) - ..data = ByteData(0); - } - _pool.clear(); - _acquireHit = 0; - _acquireMiss = 0; - _peakPoolSize = 0; - _discardedLargeBuffers = 0; - } -} - -extension type PoolStatistics(Map _stats) { - /// Number of writers currently in the pool. - int get pooled => _stats['pooled']!; - - /// Maximum pool capacity. - int get maxPoolSize => _stats['maxPoolSize']!; - - /// Initial buffer size for new writers. - int get defaultBufferSize => _stats['defaultBufferSize']!; - - /// Maximum buffer size for pooling. - int get maxReusableCapacity => _stats['maxReusableCapacity']!; - - /// Number of successful reuses from pool (cache hits). - int get acquireHit => _stats['acquireHit']!; - - /// Number of new writer allocations (cache misses). - int get acquireMiss => _stats['acquireMiss']!; - - /// Maximum pool size reached during runtime. - int get peakPoolSize => _stats['peakPoolSize']!; - - /// Number of oversized buffers discarded to prevent memory bloat. - int get discardedLargeBuffers => _stats['discardedLargeBuffers']!; - - /// Total number of acquire operations. - int get totalAcquires => acquireHit + acquireMiss; - - /// Cache hit rate (0.0 to 1.0). - double get hitRate => totalAcquires > 0 ? acquireHit / totalAcquires : 0.0; -} diff --git a/lib/src/binary_writer_pool.dart b/lib/src/binary_writer_pool.dart new file mode 100644 index 0000000..c615288 --- /dev/null +++ b/lib/src/binary_writer_pool.dart @@ -0,0 +1,279 @@ +part of 'binary_writer.dart'; + +// Disable lint to allow static-only class for pooling +/// Object pool for reusing [BinaryWriter] instances to reduce GC pressure. +/// +/// This pool maintains a cache of [BinaryWriter] instances with their +/// internal buffers, allowing efficient reuse without allocating new memory +/// for each write operation. +/// +/// ## Features +/// - **Automatic reuse:** [acquire] gets a pooled writer or creates a new one +/// - **Memory bounds:** Only reuses writers with buffers ≀ 64 KiB +/// - **Size limits:** Maintains max 32 pooled instances +/// - **Safe:** Prevents double-release and handles edge cases +/// +/// ## Usage Pattern +/// Use `acquire()` and `release()` for short-lived write operations: +/// +/// ```dart +/// final writer = BinaryWriterPool.acquire(); +/// try { +/// writer.writeUint32(42); +/// writer.writeString('Hello'); +/// final bytes = writer.toBytes(); +/// // Use bytes... +/// } finally { +/// BinaryWriterPool.release(writer); // Return to pool +/// } +/// ``` +/// +/// ## Thread Safety +/// This pool is isolate-local. Each Dart isolate maintains its own +/// static pool instance. +/// +/// Avoid sharing [BinaryWriter] instances between different isolates. +/// For concurrent operations within the same isolate, ensure writers +/// are acquired and released synchronously or protected by logic +/// to prevent interleaved usage. +/// +/// ## Performance Considerations +/// - Pooling is beneficial for high-frequency write operations +/// - Overhead is minimal for single-use writers (use regular constructor) +/// - Large buffers (>64 KiB) are discarded to avoid memory waste +/// +/// ## Memory Management +/// - Pool max size: 32 writers +/// - Max reusable buffer: 64 KiB +/// - Default buffer size: 1 KiB +/// - Use [clear] to free pooled memory explicitly +/// +/// See also: [BinaryWriter], [stats] for pool monitoring +// ignore: avoid_classes_with_only_static_members +abstract final class BinaryWriterPool { + // The internal pool of reusable writer states. + static final _pool = <_WriterState>[]; + + /// Maximum number of writers to keep in the pool. + static const _maxPoolSize = 32; + + /// Default initial buffer size for new writers (1 KiB). + static const _defaultBufferSize = 1024; + + /// Maximum buffer capacity allowed for pooling (64 KiB). + /// Writers that exceed this size are discarded to free up system memory + static const int _maxReusableCapacity = 64 * 1024; + + // Performance counters + static var _acquireHit = 0; + static var _acquireMiss = 0; + static var _peakPoolSize = 0; + static var _discardedLargeBuffers = 0; + + /// Acquires a [BinaryWriter] from the pool or creates a new one. + /// + /// Returns a pooled writer if available, otherwise creates a fresh instance + /// with the default buffer size (1 KiB). + /// + /// The returned writer is ready to use and should be returned to the pool + /// via [release] when no longer needed. + /// + /// **Best Practice:** Always use a `try-finally` block. + /// + /// There are two ways to get the data: + /// 1. Use [BinaryWriter.toBytes] if you consume data **inside** the try + /// block (zero-copy view). + /// 2. Use [BinaryWriter.takeBytes] if you need to **return** the data + /// (transfers buffer ownership). + /// + /// ```dart + /// final writer = BinaryWriterPool.acquire(); + /// try { + /// writer.writeUint32(123); + /// return writer.takeBytes(); + /// } finally { + /// BinaryWriterPool.release(writer); + /// } + /// ``` + /// + /// Returns: A [BinaryWriter] ready for use. + static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { + if (_pool.isNotEmpty) { + _acquireHit++; + final state = _pool.removeLast().._isInPool = false; + + if (state.capacity < defaultBufferSize) { + state.ensureSize(defaultBufferSize); + } + + return BinaryWriter._(state); + } + + _acquireMiss++; + + return BinaryWriter(initialBufferSize: defaultBufferSize); + } + + /// Acquires a writer, executes the given [action], and automatically + /// releases the writer back to the pool. + /// + /// This is the recommended way to use the pool as it ensures the writer + /// is always released even if an exception occurs. + /// + /// Example: + /// ```dart + /// final bytes = BinaryWriterPool.withWriter((writer) { + /// writer.writeUint32(42); + /// return writer.takeBytes(); + /// }); + /// ``` + static T withWriter( + T Function(BinaryWriter writer) action, [ + int defaultBufferSize = _defaultBufferSize, + ]) { + final writer = acquire(defaultBufferSize); + try { + return action(writer); + } finally { + release(writer); + } + } + + /// Returns a [BinaryWriter] to the pool for future reuse. + /// + /// The writer is reset (offset cleared) and stored for future [acquire] + /// calls. Writers with buffers larger than 64 KiB are not pooled to avoid + /// long-term memory retention. + /// + /// **Safe to call multiple times** (duplicate releases are ignored). + /// + /// Only writers with capacity ≀ 64 KiB are pooled. Writers exceeding this + /// limit are discarded, allowing the buffer to be garbage collected. + /// + /// **Do NOT use the writer after releasing it:** + /// + /// ```dart + /// final writer = BinaryWriterPool.acquire(); + /// writer.writeUint32(42); + /// final bytes = writer.toBytes(); + /// BinaryWriterPool.release(writer); + /// // DON'T USE writer here - it's returned to the pool and may be reused! + /// ``` + /// + /// Parameters: + /// - [writer]: The [BinaryWriter] to return to the pool + static void release(BinaryWriter writer) { + final state = writer._ws; + + // Prevent double-release and state corruption + if (state._isInPool) { + return; + } + + // Only pool writers with reasonable buffer sizes + // Prevents memory bloat from occasional large allocations + if (state.capacity <= _maxReusableCapacity && _pool.length < _maxPoolSize) { + state + ..offset = 0 + .._isInPool = true; + _pool.add(state); + + // Track peak pool size + if (_pool.length > _peakPoolSize) { + _peakPoolSize = _pool.length; + } + } else if (state.capacity > _maxReusableCapacity) { + _discardedLargeBuffers++; + } + } + + /// Returns pool statistics for monitoring and debugging. + /// + /// Useful for performance analysis and detecting pool inefficiencies. + /// + /// Returns a map with keys: + /// - `'pooled'`: Number of writers currently in the pool + /// - `'maxPoolSize'`: Maximum pool capacity + /// - `'defaultBufferSize'`: Initial buffer size for new writers + /// - `'maxReusableCapacity'`: Maximum buffer size for pooling + /// - `'acquireHit'`: Number of successful reuses from pool + /// - `'acquireMiss'`: Number of new writer allocations + /// - `'peakPoolSize'`: Maximum pool size reached + /// - `'discardedLargeBuffers'`: Number of oversized buffers discarded + /// + /// Example: + /// ```dart + /// final stats = BinaryWriterPool.stats; + /// print('Pooled writers: ${stats.pooled}'); // 5 + /// print('Hit rate: ${stats.acquireHit / (stats.acquireHit + stats.acquireMiss)}'); + /// ``` + static PoolStatistics get stats => PoolStatistics({ + 'pooled': _pool.length, + 'maxPoolSize': _maxPoolSize, + 'defaultBufferSize': _defaultBufferSize, + 'maxReusableCapacity': _maxReusableCapacity, + 'acquireHit': _acquireHit, + 'acquireMiss': _acquireMiss, + 'peakPoolSize': _peakPoolSize, + 'discardedLargeBuffers': _discardedLargeBuffers, + }); + + /// Clears the pool, releasing all cached writers. + /// + /// Use this to: + /// - Free memory during low-activity periods + /// - Reset pool state in tests + /// - Handle memory pressure + /// + /// After clearing, subsequent [acquire] calls will create new writers. + /// + /// Example: + /// ```dart + /// BinaryWriterPool.clear(); // All pooled writers discarded + /// ``` + static void clear() { + // Assist GC by breaking links to potentially large byte buffers + for (var i = 0; i < _pool.length; i++) { + _pool[i] + ..list = Uint8List(0) + ..data = ByteData(0); + } + _pool.clear(); + _acquireHit = 0; + _acquireMiss = 0; + _peakPoolSize = 0; + _discardedLargeBuffers = 0; + } +} + +extension type PoolStatistics(Map _stats) { + /// Number of writers currently in the pool. + int get pooled => _stats['pooled']!; + + /// Maximum pool capacity. + int get maxPoolSize => _stats['maxPoolSize']!; + + /// Initial buffer size for new writers. + int get defaultBufferSize => _stats['defaultBufferSize']!; + + /// Maximum buffer size for pooling. + int get maxReusableCapacity => _stats['maxReusableCapacity']!; + + /// Number of successful reuses from pool (cache hits). + int get acquireHit => _stats['acquireHit']!; + + /// Number of new writer allocations (cache misses). + int get acquireMiss => _stats['acquireMiss']!; + + /// Maximum pool size reached during runtime. + int get peakPoolSize => _stats['peakPoolSize']!; + + /// Number of oversized buffers discarded to prevent memory bloat. + int get discardedLargeBuffers => _stats['discardedLargeBuffers']!; + + /// Total number of acquire operations. + int get totalAcquires => acquireHit + acquireMiss; + + /// Cache hit rate (0.0 to 1.0). + double get hitRate => totalAcquires > 0 ? acquireHit / totalAcquires : 0.0; +} diff --git a/lib/src/string_utils.dart b/lib/src/string_utils.dart new file mode 100644 index 0000000..7a9c90f --- /dev/null +++ b/lib/src/string_utils.dart @@ -0,0 +1,95 @@ +part of 'binary_writer.dart'; + +/// Calculates the UTF-8 byte length of the given string without encoding it. +/// +/// This function efficiently computes the number of bytes required to +/// encode the string in UTF-8, taking into account multi-byte characters +/// and surrogate pairs. It's optimized with an ASCII fast path that processes +/// up to 4 ASCII characters at once. +/// +/// Useful for: +/// - Pre-allocating buffers of the correct size +/// - Calculating message sizes before serialization +/// - Validating string length constraints +/// +/// Performance: +/// - ASCII strings: ~4 bytes per loop iteration +/// - Mixed content: Falls back to character-by-character analysis +/// +/// Example: +/// ```dart +/// final text = 'Hello, δΈ–η•Œ! 🌍'; +/// final byteLength = getUtf8Length(text); // 20 bytes +/// // vs text.length would be 15 characters +/// ``` +/// +/// @param s The input string. +/// @return The number of bytes needed for UTF-8 encoding. +int getUtf8Length(String value) { + if (value.isEmpty) { + return 0; + } + + final len = value.length; + var bytes = 0; + var i = 0; + + while (i < len) { + final char = value.codeUnitAt(i); + + // ASCII fast path + if (char < 0x80) { + // Process 4 ASCII characters at a time + final end = len - 4; + while (i <= end) { + final mask = + value.codeUnitAt(i) | + value.codeUnitAt(i + 1) | + value.codeUnitAt(i + 2) | + value.codeUnitAt(i + 3); + + if (mask >= 0x80) { + break; + } + + i += 4; + bytes += 4; + } + + // Handle remaining ASCII characters + while (i < len && value.codeUnitAt(i) < 0x80) { + i++; + bytes++; + } + if (i >= len) { + return bytes; + } + continue; + } + + // 2-byte sequence + if (char < 0x800) { + bytes += 2; + i++; + } + // 3-byte sequence + else if (char >= 0xD800 && char <= 0xDBFF && i + 1 < len) { + final next = value.codeUnitAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + bytes += 4; + i += 2; + continue; + } + // Malformed surrogate pair + bytes += 3; + i++; + } + // 3-byte sequence + else { + bytes += 3; + i++; + } + } + + return bytes; +} From b4291d8ac1deb46035ce907b56df27135f7f7655 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 24 May 2026 19:13:45 +0300 Subject: [PATCH 02/10] test: discardedLargeBuffers does not increment when pool is full --- lib/src/binary_writer.dart | 2 -- test/unit/binary_writer_test.dart | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 1327b0b..2f89465 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -884,5 +884,3 @@ final class _WriterState { capacity = newCapacity; } } - - diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index 7db4397..8814a26 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -2242,6 +2242,24 @@ void main() { expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(2)); }); + test('discardedLargeBuffers does not increment when pool is full', () { + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); + + // Create 33 writers and release them all at once + // The pool can only hold 32, so the 33rd should be discarded + final writers = []; + for (var i = 0; i < 33; i++) { + writers.add(BinaryWriterPool.acquire()..writeUint32(i)); + } + for (final writer in writers) { + BinaryWriterPool.release(writer); + } + + expect(BinaryWriterPool.stats.pooled, equals(32)); + // discardedLargeBuffers only counts large buffers, not pool-full discards + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); + }); + test('totalAcquires returns sum of hits and misses', () { expect(BinaryWriterPool.stats.totalAcquires, equals(0)); From fd87e75bc8ff414a385882404384de4b7f56cff3 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 24 May 2026 19:26:31 +0300 Subject: [PATCH 03/10] cleanup --- lib/src/binary_reader.dart | 2 +- lib/src/binary_writer.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index e076dfe..e5737e7 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -732,7 +732,7 @@ final class _ReaderState { final int length; /// Current read position in the buffer. - late int offset; + int offset; /// Offset of the buffer view within its underlying [ByteBuffer]. /// Necessary for creating accurate subviews. diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 2f89465..1b0b0c3 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -790,13 +790,13 @@ final class _WriterState { } /// Current write position in the buffer. - late int offset; + int offset; /// Cached buffer capacity to avoid repeated length checks. - late int capacity; + int capacity; /// Underlying byte buffer. - late Uint8List list; + Uint8List list; /// ByteData view of the underlying buffer for efficient writes. late ByteData data; From d803e18719267f23f5a056e4029235c0f38f0055 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 24 May 2026 23:43:20 +0300 Subject: [PATCH 04/10] wip: cleanup --- lib/src/binary_reader.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index e5737e7..0fd3964 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -685,10 +685,6 @@ extension type const BinaryReader._(_ReaderState _rs) { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void _checkBounds(int bytes, String type, [int? offset]) { - if (bytes < 0) { - throw RangeError.value(bytes, 'bytes', 'Bytes must be non-negative'); - } - final start = offset ?? _rs.offset; final end = start + bytes; From bfdecefdf5029d0f6f44d7609e055c4133451d05 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Sun, 24 May 2026 23:47:49 +0300 Subject: [PATCH 05/10] Add: defaultBufferSize checker --- lib/src/binary_writer_pool.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/binary_writer_pool.dart b/lib/src/binary_writer_pool.dart index c615288..918a412 100644 --- a/lib/src/binary_writer_pool.dart +++ b/lib/src/binary_writer_pool.dart @@ -98,6 +98,14 @@ abstract final class BinaryWriterPool { /// /// Returns: A [BinaryWriter] ready for use. static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { + if (defaultBufferSize <= 0) { + throw RangeError.value( + defaultBufferSize, + 'defaultBufferSize', + 'Must be positive', + ); + } + if (_pool.isNotEmpty) { _acquireHit++; final state = _pool.removeLast().._isInPool = false; From 3cbb4643c8ae7ebe425c7892816f8b994330337e Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 00:25:45 +0300 Subject: [PATCH 06/10] wip: refactoring --- lib/src/binary_reader.dart | 2 +- lib/src/binary_writer.dart | 42 +++++++++++---------------- lib/src/binary_writer_pool.dart | 42 ++++++++++++++++++--------- lib/src/string_utils.dart | 6 ++-- test/unit/binary_writer_test.dart | 48 ++++++++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 44 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 0fd3964..8080516 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -728,7 +728,7 @@ final class _ReaderState { final int length; /// Current read position in the buffer. - int offset; + int offset; /// Offset of the buffer view within its underlying [ByteBuffer]. /// Necessary for creating accurate subviews. diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 1b0b0c3..0d58616 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -593,18 +593,7 @@ extension type BinaryWriter._(_WriterState _ws) { // Step 1: Optimistic estimation of VarInt size based on string length. // Most strings are ASCII, where byte length == character length. - int estimatedVarIntSize; - if (len < 0x80) { - estimatedVarIntSize = 1; - } else if (len < 0x4000) { - estimatedVarIntSize = 2; - } else if (len < 0x200000) { - estimatedVarIntSize = 3; - } else if (len < 0x10000000) { - estimatedVarIntSize = 4; - } else { - estimatedVarIntSize = 5; - } + final estimatedVarIntSize = _varIntSize(len); // Ensure enough space for the worst-case scenario (3 bytes per UTF-16 unit) _ws.ensureSize(estimatedVarIntSize + len * 3); @@ -619,18 +608,7 @@ extension type BinaryWriter._(_WriterState _ws) { final byteLength = _ws.offset - (startOffset + estimatedVarIntSize); // Step 4: Check if our estimate was correct for the actual byte length - int actualVarIntSize; - if (byteLength < 0x80) { - actualVarIntSize = 1; - } else if (byteLength < 0x4000) { - actualVarIntSize = 2; - } else if (byteLength < 0x200000) { - actualVarIntSize = 3; - } else if (byteLength < 0x10000000) { - actualVarIntSize = 4; - } else { - actualVarIntSize = 5; - } + final actualVarIntSize = _varIntSize(byteLength); // Step 5: If the estimate was wrong, shift the string data if (actualVarIntSize != estimatedVarIntSize) { @@ -656,6 +634,17 @@ extension type BinaryWriter._(_WriterState _ws) { _ws.offset = finalOffset; } + /// Returns the number of bytes needed to encode [value] as a VarInt. + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int _varIntSize(int value) => switch (value) { + < 0x80 => 1, + < 0x4000 => 2, + < 0x200000 => 3, + < 0x10000000 => 4, + _ => 5, + }; + /// Writes a boolean value as a single byte. /// /// `true` is written as `1` and `false` as `0`. @@ -768,9 +757,9 @@ extension type BinaryWriter._(_WriterState _ws) { /// Separated from the extension type to allow efficient inline operations. final class _WriterState { _WriterState(int initialBufferSize) - : this._validated(_validateInitialBufferSize(initialBufferSize)); + : this._fromSize(_validateInitialBufferSize(initialBufferSize)); - _WriterState._validated(int size) + _WriterState._fromSize(int size) : _size = size, capacity = (size + 63) & ~63, offset = 0, @@ -814,6 +803,7 @@ final class _WriterState { data = list.buffer.asByteData(); capacity = alignedSize; offset = 0; + _isInPool = false; } @pragma('vm:prefer-inline') diff --git a/lib/src/binary_writer_pool.dart b/lib/src/binary_writer_pool.dart index 918a412..36e3db9 100644 --- a/lib/src/binary_writer_pool.dart +++ b/lib/src/binary_writer_pool.dart @@ -58,7 +58,7 @@ abstract final class BinaryWriterPool { static const _maxPoolSize = 32; /// Default initial buffer size for new writers (1 KiB). - static const _defaultBufferSize = 1024; + static const _initialBufferSizer = 1024; /// Maximum buffer capacity allowed for pooling (64 KiB). /// Writers that exceed this size are discarded to free up system memory @@ -69,6 +69,7 @@ abstract final class BinaryWriterPool { static var _acquireMiss = 0; static var _peakPoolSize = 0; static var _discardedLargeBuffers = 0; + static var _discardedPoolFull = 0; /// Acquires a [BinaryWriter] from the pool or creates a new one. /// @@ -97,11 +98,11 @@ abstract final class BinaryWriterPool { /// ``` /// /// Returns: A [BinaryWriter] ready for use. - static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { - if (defaultBufferSize <= 0) { + static BinaryWriter acquire([int initialBufferSizer = _initialBufferSizer]) { + if (initialBufferSizer <= 0) { throw RangeError.value( - defaultBufferSize, - 'defaultBufferSize', + initialBufferSizer, + 'initialBufferSizer', 'Must be positive', ); } @@ -110,8 +111,8 @@ abstract final class BinaryWriterPool { _acquireHit++; final state = _pool.removeLast().._isInPool = false; - if (state.capacity < defaultBufferSize) { - state.ensureSize(defaultBufferSize); + if (state.capacity < initialBufferSizer) { + state.ensureSize(initialBufferSizer); } return BinaryWriter._(state); @@ -119,7 +120,7 @@ abstract final class BinaryWriterPool { _acquireMiss++; - return BinaryWriter(initialBufferSize: defaultBufferSize); + return BinaryWriter(initialBufferSize: initialBufferSizer); } /// Acquires a writer, executes the given [action], and automatically @@ -128,6 +129,10 @@ abstract final class BinaryWriterPool { /// This is the recommended way to use the pool as it ensures the writer /// is always released even if an exception occurs. /// + /// Parameters: + /// - [action]: The function to execute with the acquired writer + /// - [initialBufferSizer]: Initial buffer size for new writers (defaults to 1 KiB) + /// /// Example: /// ```dart /// final bytes = BinaryWriterPool.withWriter((writer) { @@ -137,9 +142,9 @@ abstract final class BinaryWriterPool { /// ``` static T withWriter( T Function(BinaryWriter writer) action, [ - int defaultBufferSize = _defaultBufferSize, + int initialBufferSizer = _initialBufferSizer, ]) { - final writer = acquire(defaultBufferSize); + final writer = acquire(initialBufferSizer); try { return action(writer); } finally { @@ -192,6 +197,8 @@ abstract final class BinaryWriterPool { } } else if (state.capacity > _maxReusableCapacity) { _discardedLargeBuffers++; + } else { + _discardedPoolFull++; } } @@ -202,12 +209,13 @@ abstract final class BinaryWriterPool { /// Returns a map with keys: /// - `'pooled'`: Number of writers currently in the pool /// - `'maxPoolSize'`: Maximum pool capacity - /// - `'defaultBufferSize'`: Initial buffer size for new writers + /// - `'initialBufferSizer'`: Initial buffer size for new writers /// - `'maxReusableCapacity'`: Maximum buffer size for pooling /// - `'acquireHit'`: Number of successful reuses from pool /// - `'acquireMiss'`: Number of new writer allocations /// - `'peakPoolSize'`: Maximum pool size reached /// - `'discardedLargeBuffers'`: Number of oversized buffers discarded + /// - `'discardedPoolFull'`: Number of writers discarded when pool is full /// /// Example: /// ```dart @@ -218,12 +226,13 @@ abstract final class BinaryWriterPool { static PoolStatistics get stats => PoolStatistics({ 'pooled': _pool.length, 'maxPoolSize': _maxPoolSize, - 'defaultBufferSize': _defaultBufferSize, + 'initialBufferSizer': _initialBufferSizer, 'maxReusableCapacity': _maxReusableCapacity, 'acquireHit': _acquireHit, 'acquireMiss': _acquireMiss, 'peakPoolSize': _peakPoolSize, 'discardedLargeBuffers': _discardedLargeBuffers, + 'discardedPoolFull': _discardedPoolFull, }); /// Clears the pool, releasing all cached writers. @@ -244,13 +253,15 @@ abstract final class BinaryWriterPool { for (var i = 0; i < _pool.length; i++) { _pool[i] ..list = Uint8List(0) - ..data = ByteData(0); + ..data = ByteData(0) + .._isInPool = false; } _pool.clear(); _acquireHit = 0; _acquireMiss = 0; _peakPoolSize = 0; _discardedLargeBuffers = 0; + _discardedPoolFull = 0; } } @@ -262,7 +273,7 @@ extension type PoolStatistics(Map _stats) { int get maxPoolSize => _stats['maxPoolSize']!; /// Initial buffer size for new writers. - int get defaultBufferSize => _stats['defaultBufferSize']!; + int get initialBufferSizer => _stats['initialBufferSizer']!; /// Maximum buffer size for pooling. int get maxReusableCapacity => _stats['maxReusableCapacity']!; @@ -279,6 +290,9 @@ extension type PoolStatistics(Map _stats) { /// Number of oversized buffers discarded to prevent memory bloat. int get discardedLargeBuffers => _stats['discardedLargeBuffers']!; + /// Number of writers discarded because the pool was full. + int get discardedPoolFull => _stats['discardedPoolFull']!; + /// Total number of acquire operations. int get totalAcquires => acquireHit + acquireMiss; diff --git a/lib/src/string_utils.dart b/lib/src/string_utils.dart index 7a9c90f..a6eef8e 100644 --- a/lib/src/string_utils.dart +++ b/lib/src/string_utils.dart @@ -23,8 +23,10 @@ part of 'binary_writer.dart'; /// // vs text.length would be 15 characters /// ``` /// -/// @param s The input string. -/// @return The number of bytes needed for UTF-8 encoding. +/// Parameters: +/// - [value]: The input string. +/// +/// Returns: The number of bytes needed for UTF-8 encoding. int getUtf8Length(String value) { if (value.isEmpty) { return 0; diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index 8814a26..50e37ff 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -2143,7 +2143,7 @@ void main() { expect(stats.pooled, equals(0)); expect(stats.maxPoolSize, equals(32)); - expect(stats.defaultBufferSize, equals(1024)); + expect(stats.initialBufferSizer, equals(1024)); expect(stats.maxReusableCapacity, equals(64 * 1024)); }); @@ -2260,6 +2260,52 @@ void main() { expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); }); + test('discardedPoolFull increments when pool is full', () { + expect(BinaryWriterPool.stats.discardedPoolFull, equals(0)); + + // Create 33 writers and release them all at once + // The pool can only hold 32, so the 33rd should be discarded + final writers = []; + for (var i = 0; i < 33; i++) { + writers.add(BinaryWriterPool.acquire()..writeUint32(i)); + } + for (final writer in writers) { + BinaryWriterPool.release(writer); + } + + expect(BinaryWriterPool.stats.pooled, equals(32)); + expect(BinaryWriterPool.stats.discardedPoolFull, equals(1)); + }); + + test('takeBytes() then release() works correctly', () { + final writer = BinaryWriterPool.acquire()..writeUint32(42); + final bytes = writer.takeBytes(); + expect(bytes.length, equals(4)); + expect(BinaryWriterPool.stats.pooled, equals(0)); + + BinaryWriterPool.release(writer); + expect(BinaryWriterPool.stats.pooled, equals(1)); + + final writer2 = BinaryWriterPool.acquire(); + expect(writer2, isNotNull); + BinaryWriterPool.release(writer2); + }); + + test('reset() then release() works correctly', () { + final writer = BinaryWriterPool.acquire() + ..writeUint32(42) + ..reset(); + expect(writer.bytesWritten, equals(0)); + expect(BinaryWriterPool.stats.pooled, equals(0)); + + BinaryWriterPool.release(writer); + expect(BinaryWriterPool.stats.pooled, equals(1)); + + final writer2 = BinaryWriterPool.acquire(); + expect(writer2, isNotNull); + BinaryWriterPool.release(writer2); + }); + test('totalAcquires returns sum of hits and misses', () { expect(BinaryWriterPool.stats.totalAcquires, equals(0)); From 1a1776902d481555aaa32e07e253d38b9140f737 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 00:26:29 +0300 Subject: [PATCH 07/10] dart format . --- lib/src/binary_writer_pool.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/binary_writer_pool.dart b/lib/src/binary_writer_pool.dart index 36e3db9..39f4ce3 100644 --- a/lib/src/binary_writer_pool.dart +++ b/lib/src/binary_writer_pool.dart @@ -131,7 +131,8 @@ abstract final class BinaryWriterPool { /// /// Parameters: /// - [action]: The function to execute with the acquired writer - /// - [initialBufferSizer]: Initial buffer size for new writers (defaults to 1 KiB) + /// - [initialBufferSizer]: Initial buffer size for new writers + /// (defaults to 1 KiB) /// /// Example: /// ```dart From c65078552c7d45bd5e03c37f16c194a0f9f358f6 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 00:34:56 +0300 Subject: [PATCH 08/10] =?UTF-8?q?=20added:=20`peekByte()`=20=E2=80=94=20re?= =?UTF-8?q?turns=20byte=20at=20current=20position=20without=20advancing=20?= =?UTF-8?q?offset=20update:=20Changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ lib/src/binary_reader.dart | 16 ++++++++++++++++ pubspec.yaml | 2 +- test/unit/binary_reader_test.dart | 26 ++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 852f9fb..3f70f28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 3.2.0 + +**BREAKING CHANGES:** + +- **BinaryWriterPool**: renamed `_defaultBufferSize` β†’ `_initialBufferSizer` (and parameter `defaultBufferSize` β†’ `initialBufferSizer` in `acquire()` and `withWriter()`) + +**New Features:** + +- **BinaryWriterPool**: added `_discardedPoolFull` counter β€” tracks writers discarded due to pool full (max 32) +- **BinaryWriter**: added `_varIntSize(int value)` β€” helper function for VarInt size calculation (switch expression) +- **BinaryReader**: added `peekByte()` β€” returns byte at current position without advancing offset + +**Fixes:** + +- **BinaryWriterPool**: added validation for `initialBufferSizer` in `acquire()` β€” throws `RangeError` for invalid size +- **BinaryWriterPool**: `_initializeBuffer()` now resets `_isInPool = false`, correct `takeBytes()` β†’ `release()` flow for pooled writers +- **BinaryWriterPool**: `clear()` now resets `_isInPool` for pooled writers +- **BinaryReader/BinaryWriter**: removed unnecessary `late` from `_ReaderState` and `_WriterState` (offset, capacity, list) +- **BinaryReader**: removed redundant bounds check in `peekBytes()` (already guarded by `_checkBounds`) + +**Refactoring:** + +- **_WriterState**: renamed `_validated` β†’ `_fromSize` +- **string_utils.dart**: replaced JSDoc tags `@param`/`@return` with Dart style (`Parameters:`/`Returns:`) + +**Tests:** + +- Added tests for pool statistics, edge cases takeBytes/reset/release + ## 3.1.0 - **feat**: Added `BinaryWriterPool.withWriter()` for safer and more concise object pool usage. diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 8080516..1c8a4b9 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -667,6 +667,22 @@ extension type const BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int operator [](int index) => _rs.list[index]; + /// Returns the byte at the current read position without advancing the offset. + /// + /// This is a convenience method for peeking at the next byte to be read. + /// + /// Example: + /// ```dart + /// final nextByte = reader.peekByte(); + /// if (nextByte == 0x42) { + /// // Handle type 0x42 + /// } + /// final actualByte = reader.readUint8(); // Now read it + /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + int peekByte() => _rs.list[_rs.offset]; + /// Reads [length] bytes from the current position. /// /// This is a concise alias for [readBytes]. diff --git a/pubspec.yaml b/pubspec.yaml index c85300d..03a3cb2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: pro_binary description: Efficient binary serialization library for Dart. Encodes and decodes various data types. -version: 3.1.0 +version: 3.2.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart index 91579ad..9af6d68 100644 --- a/test/unit/binary_reader_test.dart +++ b/test/unit/binary_reader_test.dart @@ -2092,6 +2092,32 @@ void main() { expect(() => reader.peekBytes(1, 11), throwsA(isA())); }, ); + + test('peekByte returns byte at current position without advancing', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.offset, equals(0)); + }); + + test('peekByte after read returns next byte', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])); + reader.readUint8(); + expect(reader.peekByte(), equals(0x43)); + expect(reader.offset, equals(1)); + }); + + test('peekByte at end returns last byte', () { + final reader = BinaryReader(Uint8List.fromList([0x42])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.offset, equals(0)); + }); + + test('peekByte multiple times returns same value', () { + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43])); + expect(reader.peekByte(), equals(0x42)); + expect(reader.peekByte(), equals(0x42)); + expect(reader.peekByte(), equals(0x42)); + }); }); }); } From 82a1efafc340798c5cd7dcfbe644e66b724dc0cc Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 00:41:52 +0300 Subject: [PATCH 09/10] =?UTF-8?q?added=20`BinaryReader.fromList(List)?= =?UTF-8?q?`=20=E2=80=94=20convenient=20constructor=20for=20`List`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + lib/src/binary_reader.dart | 17 ++++++++++++++- test/unit/binary_reader_test.dart | 35 +++++++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f70f28..364f8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - **BinaryWriterPool**: added `_discardedPoolFull` counter β€” tracks writers discarded due to pool full (max 32) - **BinaryWriter**: added `_varIntSize(int value)` β€” helper function for VarInt size calculation (switch expression) - **BinaryReader**: added `peekByte()` β€” returns byte at current position without advancing offset +- **BinaryReader**: added `BinaryReader.fromList(List)` β€” convenient constructor for `List` **Fixes:** diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 1c8a4b9..0d2b4d2 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -31,6 +31,20 @@ extension type const BinaryReader._(_ReaderState _rs) { /// bytes. BinaryReader(Uint8List buffer) : this._(_ReaderState(buffer)); + /// Creates a new [BinaryReader] from the given byte list. + /// + /// The input list will be copied into a [Uint8List] buffer. + /// This is useful when you have a [List] instead of [Uint8List]. + /// + /// Example: + /// ```dart + /// final bytes = [0x01, 0x02, 0x03, 0x04]; + /// final reader = BinaryReader.fromList(bytes); + /// final value = reader.readUint32(); + /// ``` + factory BinaryReader.fromList(List buffer) => + BinaryReader(Uint8List.fromList(buffer)); + /// Returns the number of bytes remaining to be read. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @@ -667,7 +681,8 @@ extension type const BinaryReader._(_ReaderState _rs) { @pragma('dart2js:tryInline') int operator [](int index) => _rs.list[index]; - /// Returns the byte at the current read position without advancing the offset. + /// Returns the byte at the current read position without advancing the + /// offset. /// /// This is a convenience method for peeking at the next byte to be read. /// diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart index 9af6d68..46de277 100644 --- a/test/unit/binary_reader_test.dart +++ b/test/unit/binary_reader_test.dart @@ -2100,8 +2100,8 @@ void main() { }); test('peekByte after read returns next byte', () { - final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])); - reader.readUint8(); + final reader = BinaryReader(Uint8List.fromList([0x42, 0x43, 0x44])) + ..readUint8(); expect(reader.peekByte(), equals(0x43)); expect(reader.offset, equals(1)); }); @@ -2118,6 +2118,37 @@ void main() { expect(reader.peekByte(), equals(0x42)); expect(reader.peekByte(), equals(0x42)); }); + + test('fromList creates reader from List', () { + final bytes = [0x01, 0x02, 0x03, 0x04]; + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(2)); + expect(reader.readUint8(), equals(3)); + expect(reader.readUint8(), equals(4)); + }); + + test('fromList copies data, original list can be modified', () { + final bytes = [0x01, 0x02, 0x03, 0x04]; + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint8(), equals(1)); + bytes[0] = 0xFF; + reader.reset(); + expect(reader.readUint8(), equals(1)); + }); + + test('fromList works with Uint8List', () { + final bytes = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader.fromList(bytes); + expect(reader.readUint32(), equals(0x01020304)); + }); + + test('fromList with empty list', () { + final reader = BinaryReader.fromList([]); + expect(reader.length, equals(0)); + expect(reader.availableBytes, equals(0)); + expect(reader.readUint8, throwsA(isA())); + }); }); }); } From e695f989bfb00cb6ba77b808caa3b40c5dde0cf9 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 25 May 2026 00:54:55 +0300 Subject: [PATCH 10/10] cleanup --- lib/src/binary_writer.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 0d58616..df90e39 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -761,6 +761,7 @@ final class _WriterState { _WriterState._fromSize(int size) : _size = size, + _isInPool = false, capacity = (size + 63) & ~63, offset = 0, list = Uint8List((size + 63) & ~63) { @@ -793,7 +794,9 @@ final class _WriterState { /// Initial buffer size. final int _size; - var _isInPool = false; + /// Whether this writer is currently in the pool (not available for direct + /// use). + bool _isInPool; @pragma('vm:prefer-inline') @pragma('dart2js:tryInline')