From b9459fe55f4ac2573ca665d7a72dea00a2a43f78 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 15 Dec 2025 14:34:13 +0200 Subject: [PATCH 01/29] Implement FastBinaryWriter --- lib/src/fast_binary_writer.dart | 253 ++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 lib/src/fast_binary_writer.dart diff --git a/lib/src/fast_binary_writer.dart b/lib/src/fast_binary_writer.dart new file mode 100644 index 0000000..8cf3ed0 --- /dev/null +++ b/lib/src/fast_binary_writer.dart @@ -0,0 +1,253 @@ +import 'dart:typed_data'; + +extension type const FastBinaryWriter._(_Buffer _ctx) { + FastBinaryWriter({int initialBufferSize = 128}) + : this._(_Buffer(initialBufferSize)); + + int get bytesWritten => _ctx._offset; + + void _checkRange(int value, int min, int max, String typeName) { + if (value < min || value > max) { + throw RangeError.range(value, min, max, typeName); + } + } + + void writeUint8(int value) { + _checkRange(value, 0, 255, 'Uint8'); + _ctx._ensureSize(1); + _ctx._data.setUint8(_ctx._offset, value); + _ctx._offset += 1; + } + + void writeInt8(int value) { + _checkRange(value, -128, 127, 'Int8'); + _ctx._ensureSize(1); + _ctx._data.setInt8(_ctx._offset, value); + _ctx._offset += 1; + } + + void writeUint16(int value, [Endian endian = Endian.big]) { + _checkRange(value, 0, 65535, 'Uint16'); + _ctx._ensureSize(2); + _ctx._data.setUint16(_ctx._offset, value, endian); + _ctx._offset += 2; + } + + void writeInt16(int value, [Endian endian = Endian.big]) { + _checkRange(value, -32768, 32767, 'Int16'); + _ctx._ensureSize(2); + _ctx._data.setInt16(_ctx._offset, value, endian); + _ctx._offset += 2; + } + + void writeUint32(int value, [Endian endian = Endian.big]) { + _checkRange(value, 0, 4294967295, 'Uint32'); + _ctx._ensureSize(4); + _ctx._data.setUint32(_ctx._offset, value, endian); + _ctx._offset += 4; + } + + void writeInt32(int value, [Endian endian = Endian.big]) { + _checkRange(value, -2147483648, 2147483647, 'Int32'); + _ctx._ensureSize(4); + _ctx._data.setInt32(_ctx._offset, value, endian); + _ctx._offset += 4; + } + + void writeUint64(int value, [Endian endian = Endian.big]) { + _checkRange(value, 0, 9223372036854775807, 'Uint64'); + _ctx._ensureSize(8); + _ctx._data.setUint64(_ctx._offset, value, endian); + _ctx._offset += 8; + } + + void writeInt64(int value, [Endian endian = Endian.big]) { + _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); + _ctx._ensureSize(8); + _ctx._data.setInt64(_ctx._offset, value, endian); + _ctx._offset += 8; + } + + void writeFloat32(double value, [Endian endian = Endian.big]) { + _ctx._ensureSize(4); + _ctx._data.setFloat32(_ctx._offset, value, endian); + _ctx._offset += 4; + } + + void writeFloat64(double value, [Endian endian = Endian.big]) { + _ctx._ensureSize(8); + _ctx._data.setFloat64(_ctx._offset, value, endian); + _ctx._offset += 8; + } + + void writeBytes(Iterable bytes) { + // Early return for empty byte lists + if (bytes.isEmpty) { + return; + } + + final length = bytes.length; + _ctx._ensureSize(length); + + final offset = _ctx._offset; + _ctx._list.setRange(offset, offset + length, bytes); + _ctx._offset = offset + length; + } + + void writeString(String value, {bool allowMalformed = true}) { + final len = value.length; + if (len == 0) { + return; + } + + // Optimize allocation: 3 bytes per char is enough for worst-case UTF-16 + // to UTF-8 expansion.(Surrogate pairs take 2 chars for 4 bytes = 2 + // bytes/char avg. Asian chars take 1 char for 3 bytes = 3 bytes/char avg). + _ctx._ensureSize(len * 3); + + final list = _ctx._list; + var offset = _ctx._offset; + var i = 0; + + while (i < len) { + // ------------------------------------------------------- + // ASCII Fast Path + // Loops tightly as long as characters are standard ASCII + // ------------------------------------------------------- + var c = value.codeUnitAt(i); + if (c < 128) { + // Unroll loop slightly or trust JIT/AOT to inline checking + list[offset++] = c; + i++; + // Inner loop for runs of ASCII characters + while (i < len) { + c = value.codeUnitAt(i); + if (c >= 128) { + break; + } + + list[offset++] = c; + i++; + } + + if (i == len) { + break; + } + } + + // ------------------------------------------------------- + // Multi-byte handling + // ------------------------------------------------------- + if (c < 2048) { + // 2 bytes (Cyrillic, extended Latin, etc.) + list[offset++] = 192 | (c >> 6); + list[offset++] = 128 | (c & 63); + i++; + } else if (c < 0xD800 || c > 0xDFFF) { + // 3 bytes (Standard BMP plane, excluding surrogates) + list[offset++] = 224 | (c >> 12); + list[offset++] = 128 | ((c >> 6) & 63); + list[offset++] = 128 | (c & 63); + i++; + } else { + // 4 bytes or malformed (Surrogates) + // Check for high surrogate + if (c >= 0xD800 && c <= 0xDBFF) { + if (i + 1 < len) { + final next = value.codeUnitAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + // Valid surrogate pair + final n = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); + list[offset++] = 240 | (n >> 18); + list[offset++] = 128 | ((n >> 12) & 63); + list[offset++] = 128 | ((n >> 6) & 63); + list[offset++] = 128 | (n & 63); + i += 2; + continue; + } + } + } + + // Handle error cases (Lone surrogates) + if (!allowMalformed) { + throw FormatException( + 'Invalid UTF-16: lone surrogate at index $i', + value, + i, + ); + } + + // Replacement char U+FFFD (EF BF BD) + list[offset++] = 0xEF; + list[offset++] = 0xBF; + list[offset++] = 0xBD; + i++; + } + } + + _ctx._offset = offset; + } + + Uint8List takeBytes() { + final result = Uint8List.sublistView(_ctx._list, 0, _ctx._offset); + _ctx._initializeBuffer(); + return result; + } + + Uint8List toBytes() => Uint8List.sublistView(_ctx._list, 0, _ctx._offset); + + void reset() => _ctx._initializeBuffer(); +} + +final class _Buffer { + _Buffer(int initialBufferSize) + : _size = initialBufferSize, + _capacity = initialBufferSize, + _list = Uint8List(initialBufferSize) { + _data = _list.buffer.asByteData(); + } + + /// Current write position in the buffer. + var _offset = 0; + + /// Cached buffer capacity to avoid repeated length checks. + var _capacity = 0; + + late Uint8List _list; + + late ByteData _data; + + final int _size; + + void _initializeBuffer() { + final newBuffer = Uint8List(_size); + + _list = newBuffer; + _data = newBuffer.buffer.asByteData(); + _capacity = _size; + _offset = 0; + } + + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') + void _ensureSize(int size) { + if (_offset + size <= _capacity) { + return; + } + _expand(size); + } + + void _expand(int size) { + final req = _offset + size; + var newCapacity = _capacity * 3 ~/ 2; + if (newCapacity < req) { + newCapacity = req; + } + + final newBuffer = Uint8List(newCapacity)..setRange(0, _offset, _list); + + _list = newBuffer; + _data = newBuffer.buffer.asByteData(); + _capacity = newCapacity; + } +} From f3f539d00034e3f8d02aebc7555913541b2c1086 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 22 Dec 2025 14:45:25 +0200 Subject: [PATCH 02/29] Enhance BinaryWriter performance tests and refactor for consistency - Introduced a new `FastBinaryWriterBenchmark` class to evaluate performance of the FastBinaryWriter. - Added extensive test cases for writing and reading various data types, including Uint16, Int16, Uint32, Int32, Uint64, Int64, Float32, and Float64, ensuring little-endian format is consistently used. - Updated existing tests to utilize FastBinaryWriter instead of BinaryWriter for improved performance. - Enhanced string writing tests with longer strings containing emojis and complex characters to stress UTF-8 encoding logic. - Ensured all tests validate the expected byte lengths and reset conditions after taking bytes. --- README.md | 36 ++-- example/main.dart | 4 +- lib/pro_binary.dart | 2 + lib/src/binary_reader.dart | 18 +- lib/src/binary_reader_interface.dart | 50 ++--- lib/src/binary_writer.dart | 49 ++--- lib/src/binary_writer_interface.dart | 50 ++--- lib/src/fast_binary_reader.dart | 195 ++++++++++++++++++ lib/src/fast_binary_writer.dart | 218 ++++++++++++++------ pubspec.yaml | 2 + test/binary_reader_performance_test.dart | 108 ++++++++-- test/binary_reader_test.dart | 244 +++++++++++------------ test/binary_writer_performance_test.dart | 149 ++++++++++++-- test/binary_writer_test.dart | 40 ++-- test/integration_test.dart | 80 ++++---- 15 files changed, 864 insertions(+), 381 deletions(-) create mode 100644 lib/src/fast_binary_reader.dart diff --git a/README.md b/README.md index 88fb810..3fcb230 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ import 'package:pro_binary/pro_binary.dart'; void main() { final writer = BinaryWriter() ..writeUint8(42) - ..writeUint32(1000000, Endian.little) + ..writeUint32(1000000, .little) ..writeFloat64(3.14159) ..writeString('Hello'); @@ -56,7 +56,7 @@ void main() { final reader = BinaryReader(data); final value1 = reader.readUint8(); // 42 - final value2 = reader.readUint32(Endian.little); // 1000000 + final value2 = reader.readUint32(.little); // 1000000 print('Read: $value1, $value2'); print('Remaining: ${reader.availableBytes} bytes'); @@ -73,14 +73,14 @@ final writer = BinaryWriter(initialBufferSize: 64); // Write operations writer.writeUint8(255); writer.writeInt8(-128); -writer.writeUint16(65535, Endian.big); -writer.writeInt16(-32768, Endian.big); -writer.writeUint32(4294967295, Endian.big); -writer.writeInt32(-1000, Endian.big); -writer.writeUint64(9223372036854775807, Endian.big); -writer.writeInt64(-9223372036854775808, Endian.big); -writer.writeFloat32(3.14, Endian.big); -writer.writeFloat64(3.14159, Endian.big); +writer.writeUint16(65535, .big); +writer.writeInt16(-32768, .big); +writer.writeUint32(4294967295, .big); +writer.writeInt32(-1000, .big); +writer.writeUint64(9223372036854775807, .big); +writer.writeInt64(-9223372036854775808, .big); +writer.writeFloat32(3.14, .big); +writer.writeFloat64(3.14159, .big); writer.writeBytes([1, 2, 3]); writer.writeString('text'); @@ -99,14 +99,14 @@ final reader = BinaryReader(buffer); // Read operations final u8 = reader.readUint8(); final i8 = reader.readInt8(); -final u16 = reader.readUint16(Endian.big); -final i16 = reader.readInt16(Endian.big); -final u32 = reader.readUint32(Endian.big); -final i32 = reader.readInt32(Endian.little); -final u64 = reader.readUint64(Endian.big); -final i64 = reader.readInt64(Endian.big); -final f32 = reader.readFloat32(Endian.big); -final f64 = reader.readFloat64(Endian.big); +final u16 = reader.readUint16(.big); +final i16 = reader.readInt16(.big); +final u32 = reader.readUint32(.big); +final i32 = reader.readInt32(.little); +final u64 = reader.readUint64(.big); +final i64 = reader.readInt64(.big); +final f32 = reader.readFloat32(.big); +final f64 = reader.readFloat64(.big); final bytes = reader.readBytes(10); final text = reader.readString(5); diff --git a/example/main.dart b/example/main.dart index 8919557..08ade16 100644 --- a/example/main.dart +++ b/example/main.dart @@ -17,7 +17,7 @@ void writeExample() { final writer = BinaryWriter() ..writeUint8(42) - ..writeInt32(-1000, Endian.little) + ..writeInt32(-1000, .little) ..writeFloat64(3.14159) ..writeString('Hello, World!'); @@ -37,7 +37,7 @@ void readExample() { final reader = BinaryReader(buffer); print('uint8: ${reader.readUint8()}'); - print('int32: ${reader.readInt32(Endian.little)}'); + print('int32: ${reader.readInt32(.little)}'); print('float64: ${reader.readFloat64()}'); print('string: ${reader.readString(5)}'); print('Position: ${reader.offset}/${buffer.length}\n'); diff --git a/lib/pro_binary.dart b/lib/pro_binary.dart index ed0e0c1..193b3af 100644 --- a/lib/pro_binary.dart +++ b/lib/pro_binary.dart @@ -3,3 +3,5 @@ library; export 'src/binary_reader.dart'; export 'src/binary_writer.dart'; +export 'src/fast_binary_reader.dart'; +export 'src/fast_binary_writer.dart'; diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index f692cff..aa5cd50 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -13,7 +13,7 @@ import 'binary_reader_interface.dart'; /// final value = reader.readUint32(); // 42 /// print(reader.availableBytes); // 0 /// ``` -class BinaryReader extends BinaryReaderInterface { +class BinaryReader implements BinaryReaderInterface { /// Creates a new [BinaryReader] for the given byte buffer. /// /// The [buffer] parameter must be a [Uint8List] containing the data to read. @@ -74,7 +74,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - int readUint16([Endian endian = Endian.big]) { + int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); final value = _data.getUint16(_offset, endian); @@ -86,7 +86,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - int readInt16([Endian endian = Endian.big]) { + int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); final value = _data.getInt16(_offset, endian); @@ -98,7 +98,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - int readUint32([Endian endian = Endian.big]) { + int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); final value = _data.getUint32(_offset, endian); @@ -110,7 +110,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - int readInt32([Endian endian = Endian.big]) { + int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); final value = _data.getInt32(_offset, endian); @@ -122,7 +122,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - int readUint64([Endian endian = Endian.big]) { + int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); final value = _data.getUint64(_offset, endian); @@ -134,7 +134,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - int readInt64([Endian endian = Endian.big]) { + int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); final value = _data.getInt64(_offset, endian); @@ -146,7 +146,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - double readFloat32([Endian endian = Endian.big]) { + double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); final value = _data.getFloat32(_offset, endian); @@ -158,7 +158,7 @@ class BinaryReader extends BinaryReaderInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - double readFloat64([Endian endian = Endian.big]) { + double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); final value = _data.getFloat64(_offset, endian); diff --git a/lib/src/binary_reader_interface.dart b/lib/src/binary_reader_interface.dart index fbb77cb..5049262 100644 --- a/lib/src/binary_reader_interface.dart +++ b/lib/src/binary_reader_interface.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; /// The [BinaryReaderInterface] class is an abstract base class used to decode /// various types of data from a binary format. -abstract class BinaryReaderInterface { +abstract interface class BinaryReaderInterface { /// Returns the number of bytes available to read from the buffer. /// /// This getter calculates the difference between the total length of the @@ -50,14 +50,14 @@ abstract class BinaryReaderInterface { /// /// Returns an unsigned 16-bit integer (range: 0 to 65535). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// int value = reader.readUint16(); // Reads two bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint16(Endian.little); // Reads two bytes as an unsigned integer in little-endian order. + /// int value = reader.readUint16(.little); // Reads two bytes as an unsigned integer in little-endian order. /// ``` - int readUint16([Endian endian = Endian.big]); + int readUint16([Endian endian = .big]); /// Reads a 16-bit signed integer from the buffer. /// @@ -67,14 +67,14 @@ abstract class BinaryReaderInterface { /// /// Returns a signed 16-bit integer (range: -32768 to 32767). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// int value = reader.readInt16(); // Reads two bytes as a signed integer in big-endian order. - /// int value = reader.readInt16(Endian.little); // Reads two bytes as a signed integer in little-endian order. + /// int value = reader.readInt16(.little); // Reads two bytes as a signed integer in little-endian order. /// ``` - int readInt16([Endian endian = Endian.big]); + int readInt16([Endian endian = .big]); /// Reads a 32-bit unsigned integer from the buffer. /// @@ -84,14 +84,14 @@ abstract class BinaryReaderInterface { /// /// Returns an unsigned 32-bit integer (range: 0 to 4294967295). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// int value = reader.readUint32(); // Reads four bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint32(Endian.little); // Reads four bytes as an unsigned integer in little-endian order. + /// int value = reader.readUint32(.little); // Reads four bytes as an unsigned integer in little-endian order. /// ``` - int readUint32([Endian endian = Endian.big]); + int readUint32([Endian endian = .big]); /// Reads a 32-bit signed integer from the buffer. /// @@ -101,14 +101,14 @@ abstract class BinaryReaderInterface { /// /// Returns a signed 32-bit integer (range: -2147483648 to 2147483647). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// int value = reader.readInt32(); // Reads four bytes as a signed integer in big-endian order. - /// int value = reader.readInt32(Endian.little); // Reads four bytes as a signed integer in little-endian order. + /// int value = reader.readInt32(.little); // Reads four bytes as a signed integer in little-endian order. /// ``` - int readInt32([Endian endian = Endian.big]); + int readInt32([Endian endian = .big]); /// Reads a 64-bit unsigned integer from the buffer. /// @@ -118,14 +118,14 @@ abstract class BinaryReaderInterface { /// /// Returns an unsigned 64-bit integer (range: 0 to 18446744073709551615). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// int value = reader.readUint64(); // Reads eight bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint64(Endian.little); // Reads eight bytes as an unsigned integer in little-endian order. + /// int value = reader.readUint64(.little); // Reads eight bytes as an unsigned integer in little-endian order. /// ``` - int readUint64([Endian endian = Endian.big]); + int readUint64([Endian endian = .big]); /// Reads a 64-bit signed integer from the buffer. /// @@ -136,14 +136,14 @@ abstract class BinaryReaderInterface { /// Returns a signed 64-bit integer /// (range: -9223372036854775808 to 9223372036854775807). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// int value = reader.readInt64(); // Reads eight bytes as a signed integer in big-endian order. - /// int value = reader.readInt64(Endian.little); // Reads eight bytes as a signed integer in little-endian order. + /// int value = reader.readInt64(.little); // Reads eight bytes as a signed integer in little-endian order. /// ``` - int readInt64([Endian endian = Endian.big]); + int readInt64([Endian endian = .big]); /// Reads a 32-bit floating point number from the buffer. /// @@ -152,14 +152,14 @@ abstract class BinaryReaderInterface { /// /// Returns a 32-bit floating point number. /// The optional [endian] parameter specifies the byte order to use - /// (defaults to [Endian.big]). + /// (defaults to [.big]). /// /// Example: /// ```dart /// double value = reader.readFloat32(); // Reads four bytes as a float in big-endian order. - /// double value = reader.readFloat32(Endian.little); // Reads four bytes as a float in little-endian order. + /// double value = reader.readFloat32(.little); // Reads four bytes as a float in little-endian order. /// ``` - double readFloat32([Endian endian = Endian.big]); + double readFloat32([Endian endian = .big]); /// Reads a 64-bit floating point number from the buffer. /// @@ -168,14 +168,14 @@ abstract class BinaryReaderInterface { /// /// Returns a 64-bit floating point number. /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// double value = reader.readFloat64(); // Reads eight bytes as a float in big-endian order. - /// double value = reader.readFloat64(Endian.little); // Reads eight bytes as a float in little-endian order. + /// double value = reader.readFloat64(.little); // Reads eight bytes as a float in little-endian order. /// ``` - double readFloat64([Endian endian = Endian.big]); + double readFloat64([Endian endian = .big]); /// Reads a list of bytes from the buffer. /// diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 332726a..0a9f0c5 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -14,7 +14,7 @@ import 'binary_writer_interface.dart'; /// writer.writeUint8(10); // Can continue writing /// final final = writer.takeBytes(); // View with reset /// ``` -class BinaryWriter extends BinaryWriterInterface { +class BinaryWriter implements BinaryWriterInterface { /// Creates a new [BinaryWriter] with an optional initial buffer size. /// /// The [initialBufferSize] parameter specifies the initial capacity of the @@ -62,11 +62,11 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeUint16(int value, [Endian endian = Endian.big]) { + void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); _ensureSize(2); - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = (value >> 8) & 0xFF; _buffer[_offset++] = value & 0xFF; } else { @@ -78,11 +78,11 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeInt16(int value, [Endian endian = Endian.big]) { + void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); _ensureSize(2); - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = (value >> 8) & 0xFF; _buffer[_offset++] = value & 0xFF; } else { @@ -94,11 +94,11 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeUint32(int value, [Endian endian = Endian.big]) { + void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); _ensureSize(4); - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = (value >> 24) & 0xFF; _buffer[_offset++] = (value >> 16) & 0xFF; _buffer[_offset++] = (value >> 8) & 0xFF; @@ -114,11 +114,11 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeInt32(int value, [Endian endian = Endian.big]) { + void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); _ensureSize(4); - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = (value >> 24) & 0xFF; _buffer[_offset++] = (value >> 16) & 0xFF; _buffer[_offset++] = (value >> 8) & 0xFF; @@ -134,11 +134,11 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeUint64(int value, [Endian endian = Endian.big]) { + void writeUint64(int value, [Endian endian = .big]) { _checkRange(value, 0, 9223372036854775807, 'Uint64'); _ensureSize(8); - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = (value >> 56) & 0xFF; _buffer[_offset++] = (value >> 48) & 0xFF; _buffer[_offset++] = (value >> 40) & 0xFF; @@ -162,11 +162,11 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeInt64(int value, [Endian endian = Endian.big]) { + void writeInt64(int value, [Endian endian = .big]) { _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); _ensureSize(8); - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = (value >> 56) & 0xFF; _buffer[_offset++] = (value >> 48) & 0xFF; _buffer[_offset++] = (value >> 40) & 0xFF; @@ -189,22 +189,25 @@ class BinaryWriter extends BinaryWriterInterface { // Instance-level temporary buffers for float conversion (thread-safe) final _tempU8 = Uint8List(8); - late final _tempF32 = Float32List.view(_tempU8.buffer); + final _tempU4 = Uint8List(4); + + late final _tempF32 = Float32List.view(_tempU4.buffer); late final _tempF64 = Float64List.view(_tempU8.buffer); @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeFloat32(double value, [Endian endian = Endian.big]) { + void writeFloat32(double value, [Endian endian = .big]) { _ensureSize(4); _tempF32[0] = value; // Write to temp buffer - if (endian == Endian.big) { - _buffer[_offset++] = _tempU8[3]; - _buffer[_offset++] = _tempU8[2]; - _buffer[_offset++] = _tempU8[1]; - _buffer[_offset++] = _tempU8[0]; + + if (endian == .big) { + _buffer[_offset++] = _tempU4[3]; + _buffer[_offset++] = _tempU4[2]; + _buffer[_offset++] = _tempU4[1]; + _buffer[_offset++] = _tempU4[0]; } else { - _buffer.setRange(_offset, _offset + 4, _tempU8); + _buffer.setRange(_offset, _offset + 4, _tempU4); _offset += 4; } } @@ -212,10 +215,10 @@ class BinaryWriter extends BinaryWriterInterface { @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') @override - void writeFloat64(double value, [Endian endian = Endian.big]) { + void writeFloat64(double value, [Endian endian = .big]) { _ensureSize(8); _tempF64[0] = value; - if (endian == Endian.big) { + if (endian == .big) { _buffer[_offset++] = _tempU8[7]; _buffer[_offset++] = _tempU8[6]; _buffer[_offset++] = _tempU8[5]; diff --git a/lib/src/binary_writer_interface.dart b/lib/src/binary_writer_interface.dart index f0a8c1c..e93f8dc 100644 --- a/lib/src/binary_writer_interface.dart +++ b/lib/src/binary_writer_interface.dart @@ -2,7 +2,7 @@ import 'dart:typed_data'; /// The [BinaryWriterInterface] class is an abstract base class used to encode /// various types of data into a binary format. -abstract class BinaryWriterInterface { +abstract interface class BinaryWriterInterface { /// Returns the number of bytes written to the buffer. int get bytesWritten; @@ -48,16 +48,16 @@ abstract class BinaryWriterInterface { /// The [value] parameter must be an unsigned 16-bit integer /// (range: 0 to 65535). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Throws [RangeError] if [value] is out of range. /// /// Example: /// ```dart /// writer.writeUint16(500); // Writes the value 500 as two bytes in big-endian order. - /// writer.writeUint16(500, Endian.little); // Writes the value 500 as two bytes in little-endian order. + /// writer.writeUint16(500, .little); // Writes the value 500 as two bytes in little-endian order. /// ``` - void writeUint16(int value, [Endian endian = Endian.big]); + void writeUint16(int value, [Endian endian = .big]); /// Writes a 16-bit signed integer to the buffer. /// @@ -69,16 +69,16 @@ abstract class BinaryWriterInterface { /// The [value] parameter must be a signed 16-bit integer /// (range: -32768 to 32767). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Throws [RangeError] if [value] is out of range. /// /// Example: /// ```dart /// writer.writeInt16(-100); // Writes the value -100 as two bytes in big-endian order. - /// writer.writeInt16(-100, Endian.little); // Writes the value -100 as two bytes in little-endian order. + /// writer.writeInt16(-100, .little); // Writes the value -100 as two bytes in little-endian order. /// ``` - void writeInt16(int value, [Endian endian = Endian.big]); + void writeInt16(int value, [Endian endian = .big]); /// Writes a 32-bit unsigned integer to the buffer. /// @@ -90,16 +90,16 @@ abstract class BinaryWriterInterface { /// The [value] parameter must be an unsigned 32-bit integer /// (range: 0 to 4294967295). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Throws [RangeError] if [value] is out of range. /// /// Example: /// ```dart /// writer.writeUint32(100000); // Writes the value 100000 as four bytes in big-endian order. - /// writer.writeUint32(100000, Endian.little); // Writes the value 100000 as four bytes in little-endian order. + /// writer.writeUint32(100000, .little); // Writes the value 100000 as four bytes in little-endian order. /// ``` - void writeUint32(int value, [Endian endian = Endian.big]); + void writeUint32(int value, [Endian endian = .big]); /// Writes a 32-bit signed integer to the buffer. /// @@ -111,14 +111,14 @@ abstract class BinaryWriterInterface { /// The [value] parameter must be a signed 32-bit integer /// (range: -2147483648 to 2147483647). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// writer.writeInt32(-50000); // Writes the value -50000 as four bytes in big-endian order. - /// writer.writeInt32(-50000, Endian.little); // Writes the value -50000 as four bytes in little-endian order. + /// writer.writeInt32(-50000, .little); // Writes the value -50000 as four bytes in little-endian order. /// ``` - void writeInt32(int value, [Endian endian = Endian.big]); + void writeInt32(int value, [Endian endian = .big]); /// Writes a 64-bit unsigned integer to the buffer. /// @@ -131,16 +131,16 @@ abstract class BinaryWriterInterface { /// The [value] parameter must be an unsigned 64-bit integer /// (range: 0 to 18446744073709551615). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Throws [RangeError] if [value] is out of range. /// /// Example: /// ```dart /// writer.writeUint64(10000000000); // Writes the value 10000000000 as eight bytes in big-endian order. - /// writer.writeUint64(10000000000, Endian.little); // Writes the value 10000000000 as eight bytes in little-endian order. + /// writer.writeUint64(10000000000, .little); // Writes the value 10000000000 as eight bytes in little-endian order. /// ``` - void writeUint64(int value, [Endian endian = Endian.big]); + void writeUint64(int value, [Endian endian = .big]); /// Writes a 64-bit signed integer to the buffer. /// @@ -153,16 +153,16 @@ abstract class BinaryWriterInterface { /// The [value] parameter must be a signed 64-bit integer /// (range: -9223372036854775808 to 9223372036854775807). /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Throws [RangeError] if [value] is out of range. /// /// Example: /// ```dart /// writer.writeInt64(-10000000000); // Writes the value -10000000000 as eight bytes in big-endian order. - /// writer.writeInt64(-10000000000, Endian.little); // Writes the value -10000000000 as eight bytes in little-endian order. + /// writer.writeInt64(-10000000000, .little); // Writes the value -10000000000 as eight bytes in little-endian order. /// ``` - void writeInt64(int value, [Endian endian = Endian.big]); + void writeInt64(int value, [Endian endian = .big]); /// Writes a 32-bit floating point number to the buffer. /// @@ -173,16 +173,16 @@ abstract class BinaryWriterInterface { /// /// The [value] parameter must be a 32-bit floating point number. /// The optional [endian] parameter specifies the byte order to use - /// (defaults to [Endian.big]). + /// (defaults to [.big]). /// /// Throws [RangeError] if [value] is out of range. /// /// Example: /// ```dart /// writer.writeFloat32(3.14); // Writes the value 3.14 as four bytes in big-endian order. - /// writer.writeFloat32(3.14, Endian.little); // Writes the value 3.14 as four bytes in little-endian order. + /// writer.writeFloat32(3.14, .little); // Writes the value 3.14 as four bytes in little-endian order. /// ``` - void writeFloat32(double value, [Endian endian = Endian.big]); + void writeFloat32(double value, [Endian endian = .big]); /// Writes a 64-bit floating point number to the buffer. /// @@ -193,14 +193,14 @@ abstract class BinaryWriterInterface { /// /// The [value] parameter must be a 64-bit floating point number. /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [Endian.big]). + /// to [.big]). /// /// Example: /// ```dart /// writer.writeFloat64(3.14); // Writes the value 3.14 as eight bytes in big-endian order. - /// writer.writeFloat64(3.14, Endian.little); // Writes the value 3.14 as eight bytes in little-endian order. + /// writer.writeFloat64(3.14, .little); // Writes the value 3.14 as eight bytes in little-endian order. /// ``` - void writeFloat64(double value, [Endian endian = Endian.big]); + void writeFloat64(double value, [Endian endian = .big]); /// Writes a list of bytes to the buffer. /// diff --git a/lib/src/fast_binary_reader.dart b/lib/src/fast_binary_reader.dart new file mode 100644 index 0000000..a713419 --- /dev/null +++ b/lib/src/fast_binary_reader.dart @@ -0,0 +1,195 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +extension type const FastBinaryReader._(_Buffer _ctx) { + FastBinaryReader(Uint8List buffer) : this._(_Buffer(buffer)); + + @pragma('vm:prefer-inline') + int get availableBytes => _ctx.length - _ctx.offset; + + @pragma('vm:prefer-inline') + int get lengthInBytes => _ctx.lengthInBytes; + + @pragma('vm:prefer-inline') + int get offset => _ctx.offset; + + @pragma('vm:prefer-inline') + int get length => _ctx.length; + + @pragma('vm:prefer-inline') + int get _offset => _ctx.offset; + + @pragma('vm:prefer-inline') + set _offset(int value) { + _ctx.offset = value; + } + + @pragma('vm:prefer-inline') + ByteData get _data => _ctx.data; + + @pragma('vm:prefer-inline') + void _checkBounds(int bytes, String type, [int? offset]) { + assert( + (offset ?? _offset) + bytes <= _ctx.length, + 'Not enough bytes to read $type: required $bytes bytes, available ' + '${_ctx.length - _offset} bytes at offset $_offset', + ); + } + + @pragma('vm:prefer-inline') + int readUint8() { + _checkBounds(1, 'Uint8'); + + return _data.getUint8(_offset++); + } + + @pragma('vm:prefer-inline') + int readInt8() { + _checkBounds(1, 'Int8'); + + return _data.getInt8(_offset++); + } + + @pragma('vm:prefer-inline') + int readUint16([Endian endian = .big]) { + _checkBounds(2, 'Uint16'); + + final value = _data.getUint16(_offset, endian); + _offset += 2; + + return value; + } + + @pragma('vm:prefer-inline') + int readInt16([Endian endian = .big]) { + _checkBounds(2, 'Int16'); + + final value = _data.getInt16(_offset, endian); + _offset += 2; + + return value; + } + + @pragma('vm:prefer-inline') + int readUint32([Endian endian = .big]) { + _checkBounds(4, 'Uint32'); + + final value = _data.getUint32(_offset, endian); + _offset += 4; + return value; + } + + @pragma('vm:prefer-inline') + int readInt32([Endian endian = .big]) { + _checkBounds(4, 'Int32'); + final value = _data.getInt32(_offset, endian); + _offset += 4; + return value; + } + + @pragma('vm:prefer-inline') + int readUint64([Endian endian = .big]) { + _checkBounds(8, 'Uint64'); + final value = _data.getUint64(_offset, endian); + _offset += 8; + return value; + } + + @pragma('vm:prefer-inline') + int readInt64([Endian endian = .big]) { + _checkBounds(8, 'Int64'); + final value = _data.getInt64(_offset, endian); + _offset += 8; + return value; + } + + @pragma('vm:prefer-inline') + double readFloat32([Endian endian = .big]) { + _checkBounds(4, 'Float32'); + + final value = _data.getFloat32(_offset, endian); + _offset += 4; + + return value; + } + + @pragma('vm:prefer-inline') + double readFloat64([Endian endian = .big]) { + _checkBounds(8, 'Float64'); + + final value = _data.getFloat64(_offset, endian); + _offset += 8; + return value; + } + + @pragma('vm:prefer-inline') + Uint8List readBytes(int length) { + assert(length >= 0, 'Length must be non-negative'); + _checkBounds(length, 'Bytes'); + + final bytes = _data.buffer.asUint8List(_offset, length); + _offset += length; + + return bytes; + } + + @pragma('vm:prefer-inline') + String readString(int length, {bool allowMalformed = false}) { + if (length == 0) { + return ''; + } + + _checkBounds(length, 'String'); + + final view = _data.buffer.asUint8List(_offset, length); + _offset += length; + + return utf8.decode(view, allowMalformed: allowMalformed); + } + + + @pragma('vm:prefer-inline') + Uint8List peekBytes(int length, [int? offset]) { + assert(length >= 0, 'Length must be non-negative'); + + if (length == 0) { + return Uint8List(0); + } + + final peekOffset = offset ?? _offset; + _checkBounds(length, 'Peek Bytes', peekOffset); + + return _data.buffer.asUint8List(peekOffset, length); + } + + void skip(int length) { + assert(length >= 0, 'Length must be non-negative'); + _checkBounds(length, 'Skip'); + + _offset += length; + } + + @pragma('vm:prefer-inline') + void reset() { + _offset = 0; + } +} + +final class _Buffer { + _Buffer(Uint8List buffer) + : data = ByteData.sublistView(buffer).asUnmodifiableView(), + length = buffer.length, + lengthInBytes = buffer.lengthInBytes, + offset = 0; + + /// Efficient view for typed data access. + final ByteData data; + + /// Total length of the buffer. + final int length; + + /// Current read position in the buffer. + late int offset; + + final int lengthInBytes; +} diff --git a/lib/src/fast_binary_writer.dart b/lib/src/fast_binary_writer.dart index 8cf3ed0..cc4daf1 100644 --- a/lib/src/fast_binary_writer.dart +++ b/lib/src/fast_binary_writer.dart @@ -1,87 +1,182 @@ import 'dart:typed_data'; -extension type const FastBinaryWriter._(_Buffer _ctx) { +extension type FastBinaryWriter._(_Buffer _ctx) { FastBinaryWriter({int initialBufferSize = 128}) : this._(_Buffer(initialBufferSize)); - int get bytesWritten => _ctx._offset; + int get bytesWritten => _ctx.offset; + @pragma('vm:prefer-inline') void _checkRange(int value, int min, int max, String typeName) { if (value < min || value > max) { throw RangeError.range(value, min, max, typeName); } } + @pragma('vm:prefer-inline') void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); _ctx._ensureSize(1); - _ctx._data.setUint8(_ctx._offset, value); - _ctx._offset += 1; + _ctx.list[_ctx.offset++] = value; } + @pragma('vm:prefer-inline') void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); _ctx._ensureSize(1); - _ctx._data.setInt8(_ctx._offset, value); - _ctx._offset += 1; + _ctx.list[_ctx.offset++] = value & 0xFF; } - void writeUint16(int value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); _ctx._ensureSize(2); - _ctx._data.setUint16(_ctx._offset, value, endian); - _ctx._offset += 2; + + final list = _ctx.list; + var offset = _ctx.offset; + if (endian == .big) { + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = value & 0xFF; + } else { + list[offset++] = value & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + } + _ctx.offset = offset; } - void writeInt16(int value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); _ctx._ensureSize(2); - _ctx._data.setInt16(_ctx._offset, value, endian); - _ctx._offset += 2; + + final list = _ctx.list; + var offset = _ctx.offset; + if (endian == .big) { + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = value & 0xFF; + } else { + list[offset++] = value & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + } + _ctx.offset = offset; } - void writeUint32(int value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); _ctx._ensureSize(4); - _ctx._data.setUint32(_ctx._offset, value, endian); - _ctx._offset += 4; + + final list = _ctx.list; + var offset = _ctx.offset; + if (endian == .big) { + list[offset++] = (value >> 24) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = value & 0xFF; + } else { + list[offset++] = value & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 24) & 0xFF; + } + _ctx.offset = offset; } - void writeInt32(int value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); _ctx._ensureSize(4); - _ctx._data.setInt32(_ctx._offset, value, endian); - _ctx._offset += 4; + + final list = _ctx.list; + var offset = _ctx.offset; + if (endian == .big) { + list[offset++] = (value >> 24) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = value & 0xFF; + } else { + list[offset++] = value & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 24) & 0xFF; + } + _ctx.offset = offset; } - void writeUint64(int value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeUint64(int value, [Endian endian = .big]) { _checkRange(value, 0, 9223372036854775807, 'Uint64'); _ctx._ensureSize(8); - _ctx._data.setUint64(_ctx._offset, value, endian); - _ctx._offset += 8; + + final list = _ctx.list; + var offset = _ctx.offset; + if (endian == .big) { + list[offset++] = (value >> 56) & 0xFF; + list[offset++] = (value >> 48) & 0xFF; + list[offset++] = (value >> 40) & 0xFF; + list[offset++] = (value >> 32) & 0xFF; + list[offset++] = (value >> 24) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = value & 0xFF; + } else { + list[offset++] = value & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 24) & 0xFF; + list[offset++] = (value >> 32) & 0xFF; + list[offset++] = (value >> 40) & 0xFF; + list[offset++] = (value >> 48) & 0xFF; + list[offset++] = (value >> 56) & 0xFF; + } + _ctx.offset = offset; } - void writeInt64(int value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeInt64(int value, [Endian endian = .big]) { _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); _ctx._ensureSize(8); - _ctx._data.setInt64(_ctx._offset, value, endian); - _ctx._offset += 8; + + final list = _ctx.list; + var offset = _ctx.offset; + if (endian == .big) { + list[offset++] = (value >> 56) & 0xFF; + list[offset++] = (value >> 48) & 0xFF; + list[offset++] = (value >> 40) & 0xFF; + list[offset++] = (value >> 32) & 0xFF; + list[offset++] = (value >> 24) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = value & 0xFF; + } else { + list[offset++] = value & 0xFF; + list[offset++] = (value >> 8) & 0xFF; + list[offset++] = (value >> 16) & 0xFF; + list[offset++] = (value >> 24) & 0xFF; + list[offset++] = (value >> 32) & 0xFF; + list[offset++] = (value >> 40) & 0xFF; + list[offset++] = (value >> 48) & 0xFF; + list[offset++] = (value >> 56) & 0xFF; + } + _ctx.offset = offset; } - void writeFloat32(double value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeFloat32(double value, [Endian endian = .big]) { _ctx._ensureSize(4); - _ctx._data.setFloat32(_ctx._offset, value, endian); - _ctx._offset += 4; + _ctx.data.setFloat32(_ctx.offset, value, endian); + _ctx.offset += 4; } - void writeFloat64(double value, [Endian endian = Endian.big]) { + @pragma('vm:prefer-inline') + void writeFloat64(double value, [Endian endian = .big]) { _ctx._ensureSize(8); - _ctx._data.setFloat64(_ctx._offset, value, endian); - _ctx._offset += 8; + _ctx.data.setFloat64(_ctx.offset, value, endian); + _ctx.offset += 8; } + @pragma('vm:prefer-inline') void writeBytes(Iterable bytes) { - // Early return for empty byte lists if (bytes.isEmpty) { return; } @@ -89,11 +184,12 @@ extension type const FastBinaryWriter._(_Buffer _ctx) { final length = bytes.length; _ctx._ensureSize(length); - final offset = _ctx._offset; - _ctx._list.setRange(offset, offset + length, bytes); - _ctx._offset = offset + length; + final offset = _ctx.offset; + _ctx.list.setRange(offset, offset + length, bytes); + _ctx.offset = offset + length; } + @pragma('vm:prefer-inline') void writeString(String value, {bool allowMalformed = true}) { final len = value.length; if (len == 0) { @@ -105,8 +201,8 @@ extension type const FastBinaryWriter._(_Buffer _ctx) { // bytes/char avg. Asian chars take 1 char for 3 bytes = 3 bytes/char avg). _ctx._ensureSize(len * 3); - final list = _ctx._list; - var offset = _ctx._offset; + final list = _ctx.list; + var offset = _ctx.offset; var i = 0; while (i < len) { @@ -185,69 +281,77 @@ extension type const FastBinaryWriter._(_Buffer _ctx) { } } - _ctx._offset = offset; + _ctx.offset = offset; } + @pragma('vm:prefer-inline') Uint8List takeBytes() { - final result = Uint8List.sublistView(_ctx._list, 0, _ctx._offset); + final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); _ctx._initializeBuffer(); return result; } - Uint8List toBytes() => Uint8List.sublistView(_ctx._list, 0, _ctx._offset); + @pragma('vm:prefer-inline') + Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); + @pragma('vm:prefer-inline') void reset() => _ctx._initializeBuffer(); } final class _Buffer { _Buffer(int initialBufferSize) : _size = initialBufferSize, - _capacity = initialBufferSize, - _list = Uint8List(initialBufferSize) { - _data = _list.buffer.asByteData(); + capacity = initialBufferSize, + offset = 0, + list = Uint8List(initialBufferSize) { + data = list.buffer.asByteData(); } /// Current write position in the buffer. - var _offset = 0; + late int offset; /// Cached buffer capacity to avoid repeated length checks. - var _capacity = 0; + late int capacity; - late Uint8List _list; + /// Underlying byte buffer. + late Uint8List list; - late ByteData _data; + /// ByteData view of the underlying buffer for efficient writes. + late ByteData data; + /// Initial buffer size. final int _size; + @pragma('vm:prefer-inline') void _initializeBuffer() { final newBuffer = Uint8List(_size); - _list = newBuffer; - _data = newBuffer.buffer.asByteData(); - _capacity = _size; - _offset = 0; + list = newBuffer; + capacity = _size; + offset = 0; } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') void _ensureSize(int size) { - if (_offset + size <= _capacity) { + if (offset + size <= capacity) { return; } + _expand(size); } void _expand(int size) { - final req = _offset + size; - var newCapacity = _capacity * 3 ~/ 2; + final req = offset + size; + var newCapacity = capacity * 3 ~/ 2; if (newCapacity < req) { newCapacity = req; } - final newBuffer = Uint8List(newCapacity)..setRange(0, _offset, _list); + final list = Uint8List(newCapacity)..setRange(0, offset, this.list); - _list = newBuffer; - _data = newBuffer.buffer.asByteData(); - _capacity = newCapacity; + this.list = list; + data = list.buffer.asByteData(0, newCapacity); + capacity = newCapacity; } } diff --git a/pubspec.yaml b/pubspec.yaml index c955e15..c8b216a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,4 +28,6 @@ dev_dependencies: benchmark_harness: ^2.4.0 pro_lints: ^5.1.0 test: ^1.28.0 +dependencies: + meta: ^1.17.0 diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart index cc59edc..03fc930 100644 --- a/test/binary_reader_performance_test.dart +++ b/test/binary_reader_performance_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; @@ -15,17 +13,17 @@ class BinaryReaderBenchmark extends BenchmarkBase { 'Some more data to increase buffer usage. ' 'The quick brown fox jumps over the lazy dog.'; - final writer = BinaryWriter() + final writer = FastBinaryWriter() ..writeUint8(42) ..writeInt8(-42) - ..writeUint16(65535, Endian.little) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648, Endian.little) - ..writeUint64(9223372036854775807, Endian.little) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(3.14, Endian.little) - ..writeFloat64(3.141592653589793, Endian.little) + ..writeUint16(65535, .little) + ..writeInt16(-32768, .little) + ..writeUint32(4294967295, .little) + ..writeInt32(-2147483648, .little) + ..writeUint64(9223372036854775807, .little) + ..writeInt64(-9223372036854775808, .little) + ..writeFloat32(3.14, .little) + ..writeFloat64(3.141592653589793, .little) ..writeFloat64(2.718281828459045) ..writeInt8(string.length) ..writeString(string) @@ -46,15 +44,15 @@ class BinaryReaderBenchmark extends BenchmarkBase { for (var i = 0; i < 1000; i++) { final _ = reader.readUint8(); final _ = reader.readInt8(); - final _ = reader.readUint16(Endian.little); - final _ = reader.readInt16(Endian.little); - final _ = reader.readUint32(Endian.little); - final _ = reader.readInt32(Endian.little); - final _ = reader.readUint64(Endian.little); - final _ = reader.readInt64(Endian.little); - final _ = reader.readFloat32(Endian.little); - final _ = reader.readFloat64(Endian.little); - final _ = reader.readFloat64(Endian.little); + final _ = reader.readUint16(.little); + final _ = reader.readInt16(.little); + final _ = reader.readUint32(.little); + final _ = reader.readInt32(.little); + final _ = reader.readUint64(.little); + final _ = reader.readInt64(.little); + final _ = reader.readFloat32(.little); + final _ = reader.readFloat64(.little); + final _ = reader.readFloat64(.little); final length = reader.readInt8(); final _ = reader.readString(length); final longLength = reader.readInt32(); @@ -72,6 +70,76 @@ class BinaryReaderBenchmark extends BenchmarkBase { } } +class FastBinaryReaderBenchmark extends BenchmarkBase { + FastBinaryReaderBenchmark() : super('FastBinaryReader performance test'); + + late final FastBinaryReader reader; + + @override + void setup() { + const string = 'Hello, World!'; + const longString = + 'Some more data to increase buffer usage. ' + 'The quick brown fox jumps over the lazy dog.'; + + final writer = FastBinaryWriter() + ..writeUint8(42) + ..writeInt8(-42) + ..writeUint16(65535, .little) + ..writeInt16(-32768, .little) + ..writeUint32(4294967295, .little) + ..writeInt32(-2147483648, .little) + ..writeUint64(9223372036854775807, .little) + ..writeInt64(-9223372036854775808, .little) + ..writeFloat32(3.14, .little) + ..writeFloat64(3.141592653589793, .little) + ..writeFloat64(2.718281828459045) + ..writeInt8(string.length) + ..writeString(string) + ..writeInt32(longString.length) + ..writeString(longString) + ..writeBytes([]) + ..writeBytes(List.filled(120, 100)); + + final buffer = writer.takeBytes(); + reader = FastBinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readUint8(); + final _ = reader.readInt8(); + final _ = reader.readUint16(.little); + final _ = reader.readInt16(.little); + final _ = reader.readUint32(.little); + final _ = reader.readInt32(.little); + final _ = reader.readUint64(.little); + final _ = reader.readInt64(.little); + final _ = reader.readFloat32(.little); + final _ = reader.readFloat64(.little); + final _ = reader.readFloat64(.little); + final length = reader.readInt8(); + final _ = reader.readString(length); + final longLength = reader.readInt32(); + final _ = reader.readString(longLength); + final _ = reader.readBytes(0); + final _ = reader.readBytes(120); + + assert(reader.availableBytes == 0, 'Not all bytes were read'); + reader.reset(); + } + } + + static void main() { + FastBinaryReaderBenchmark().report(); + } +} + void main() { BinaryReaderBenchmark.main(); + FastBinaryReaderBenchmark.main(); } diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index a122d81..56fff59 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -5,10 +5,10 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('BinaryReader', () { + group('FastBinaryReader', () { test('readUint8', () { final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8(), equals(1)); expect(reader.availableBytes, equals(0)); @@ -16,7 +16,7 @@ void main() { test('readInt8', () { final buffer = Uint8List.fromList([0xFF]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt8(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -24,7 +24,7 @@ void main() { test('readUint16 big-endian', () { final buffer = Uint8List.fromList([0x01, 0x00]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint16(), equals(256)); expect(reader.availableBytes, equals(0)); @@ -32,15 +32,15 @@ void main() { test('readUint16 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readUint16(Endian.little), equals(256)); + expect(reader.readUint16(.little), equals(256)); expect(reader.availableBytes, equals(0)); }); test('readInt16 big-endian', () { final buffer = Uint8List.fromList([0xFF, 0xFF]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt16(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -48,15 +48,15 @@ void main() { test('readInt16 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x80]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readInt16(Endian.little), equals(-32768)); + expect(reader.readInt16(.little), equals(-32768)); expect(reader.availableBytes, equals(0)); }); test('readUint32 big-endian', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x00, 0x00]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint32(), equals(65536)); expect(reader.availableBytes, equals(0)); @@ -64,15 +64,15 @@ void main() { test('readUint32 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x00, 0x01, 0x00]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readUint32(Endian.little), equals(65536)); + expect(reader.readUint32(.little), equals(65536)); expect(reader.availableBytes, equals(0)); }); test('readInt32 big-endian', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt32(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -80,9 +80,9 @@ void main() { test('readInt32 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x00, 0x00, 0x80]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readInt32(Endian.little), equals(-2147483648)); + expect(reader.readInt32(.little), equals(-2147483648)); expect(reader.availableBytes, equals(0)); }); @@ -97,7 +97,7 @@ void main() { 0x00, 0x00, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint64(), equals(4294967296)); expect(reader.availableBytes, equals(0)); @@ -114,9 +114,9 @@ void main() { 0x00, 0x00, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readUint64(Endian.little), equals(4294967296)); + expect(reader.readUint64(.little), equals(4294967296)); expect(reader.availableBytes, equals(0)); }); @@ -131,7 +131,7 @@ void main() { 0xFF, 0xFF, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt64(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -148,15 +148,15 @@ void main() { 0x00, 0x80, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readInt64(Endian.little), equals(-9223372036854775808)); + expect(reader.readInt64(.little), equals(-9223372036854775808)); expect(reader.availableBytes, equals(0)); }); test('readFloat32 big-endian', () { final buffer = Uint8List.fromList([0x40, 0x49, 0x0F, 0xDB]); // 3.1415927 - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat32(), closeTo(3.1415927, 0.0000001)); expect(reader.availableBytes, equals(0)); @@ -164,9 +164,9 @@ void main() { test('readFloat32 little-endian', () { final buffer = Uint8List.fromList([0xDB, 0x0F, 0x49, 0x40]); // 3.1415927 - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.readFloat32(Endian.little), closeTo(3.1415927, 0.0000001)); + expect(reader.readFloat32(.little), closeTo(3.1415927, 0.0000001)); expect(reader.availableBytes, equals(0)); }); @@ -181,7 +181,7 @@ void main() { 0x2D, 0x18, ]); // 3.141592653589793 - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect( reader.readFloat64(), @@ -201,10 +201,10 @@ void main() { 0x09, 0x40, ]); // 3.141592653589793 - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect( - reader.readFloat64(Endian.little), + reader.readFloat64(.little), closeTo(3.141592653589793, 0.000000000000001), ); expect(reader.availableBytes, equals(0)); @@ -213,7 +213,7 @@ void main() { test('readBytes', () { final data = [0x01, 0x02, 0x03, 0x04, 0x05]; final buffer = Uint8List.fromList(data); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readBytes(5), equals(data)); expect(reader.availableBytes, equals(0)); @@ -223,7 +223,7 @@ void main() { const str = 'Hello, world!'; final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); expect(reader.availableBytes, equals(0)); @@ -233,7 +233,7 @@ void main() { const str = 'Привет, мир!'; // "Hello, world!" in Russian final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); expect(reader.availableBytes, equals(0)); @@ -241,7 +241,7 @@ void main() { test('availableBytes returns correct number of remaining bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.availableBytes, equals(4)); reader.readUint8(); @@ -252,42 +252,41 @@ void main() { test('usedBytes returns correct number of used bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); - expect(reader.usedBytes, equals(0)); + expect(reader.offset, equals(0)); reader.readUint8(); - expect(reader.usedBytes, equals(1)); + expect(reader.offset, equals(1)); reader.readBytes(2); - expect(reader.usedBytes, equals(3)); + expect(reader.offset, equals(3)); }); test( 'peekBytes returns correct bytes without changing the internal state', () { final buffer = Uint8List.fromList([0x10, 0x20, 0x30, 0x40, 0x50]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final peekedBytes = reader.peekBytes(3); expect(peekedBytes, equals([0x10, 0x20, 0x30])); - expect(reader.usedBytes, equals(0)); - + expect(reader.offset, equals(0)); reader.readUint8(); // Now usedBytes should be 1 final peekedBytesWithOffset = reader.peekBytes(2, 2); expect(peekedBytesWithOffset, equals([0x30, 0x40])); - expect(reader.usedBytes, equals(1)); + expect(reader.offset, equals(1)); }, ); test('skip method correctly updates the offset', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer)..skip(2); - expect(reader.usedBytes, equals(2)); + final reader = FastBinaryReader(buffer)..skip(2); + expect(reader.offset, equals(2)); expect(reader.readUint8(), equals(0x02)); }); test('read zero-length bytes', () { final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readBytes(0), equals([])); expect(reader.availableBytes, equals(0)); @@ -295,14 +294,14 @@ void main() { test('read beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint32, throwsA(isA())); }); test('negative length input throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.readBytes(-1), throwsA(isA())); expect(() => reader.skip(-5), throwsA(isA())); @@ -311,21 +310,21 @@ void main() { test('reading from empty buffer', () { final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8, throwsA(isA())); }); test('reading with offset at end of buffer', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer)..skip(2); + final reader = FastBinaryReader(buffer)..skip(2); expect(reader.readUint8, throwsA(isA())); }); test('peekBytes beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.peekBytes(3), throwsA(isA())); expect(() => reader.peekBytes(1, 2), throwsA(isA())); @@ -333,21 +332,21 @@ void main() { test('readString with insufficient bytes throws AssertionError', () { final buffer = Uint8List.fromList([0x48, 0x65]); // 'He' - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.readString(5), throwsA(isA())); }); test('readBytes with insufficient bytes throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.readBytes(3), throwsA(isA())); }); test('read methods throw AssertionError when not enough bytes', () { final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint32, throwsA(isA())); expect(reader.readInt32, throwsA(isA())); @@ -358,7 +357,7 @@ void main() { 'readUint64 and readInt64 with insufficient bytes throw AssertionError', () { final buffer = Uint8List.fromList(List.filled(7, 0x00)); // Only 7 bytes - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint64, throwsA(isA())); expect(reader.readInt64, throwsA(isA())); @@ -367,7 +366,7 @@ void main() { test('skip beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.skip(3), throwsA(isA())); }); @@ -382,7 +381,7 @@ void main() { 0xFF, 0xFF, 0xFF, 0xFF, // Int32 big-endian 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Float64 (double 2.0) ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8(), equals(0x01)); expect(reader.readInt8(), equals(-1)); @@ -397,7 +396,7 @@ void main() { const str = 'こんにちは世界'; // "Hello, World" in Japanese final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); }); @@ -405,42 +404,42 @@ void main() { group('Boundary checks', () { test('readUint8 throws when buffer is empty', () { final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8, throwsA(isA())); }); test('readInt8 throws when buffer is empty', () { final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt8, throwsA(isA())); }); test('readUint16 throws when only 1 byte available', () { final buffer = Uint8List.fromList([0x01]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint16, throwsA(isA())); }); test('readInt16 throws when only 1 byte available', () { final buffer = Uint8List.fromList([0xFF]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt16, throwsA(isA())); }); test('readUint32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint32, throwsA(isA())); }); test('readInt32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt32, throwsA(isA())); }); @@ -455,7 +454,7 @@ void main() { 0x06, 0x07, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint64, throwsA(isA())); }); @@ -470,14 +469,14 @@ void main() { 0xFF, 0xFF, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readInt64, throwsA(isA())); }); test('readFloat32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat32, throwsA(isA())); }); @@ -492,35 +491,35 @@ void main() { 0x06, 0x07, ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat64, throwsA(isA())); }); test('readBytes throws when requested length exceeds available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.readBytes(5), throwsA(isA())); }); test('readBytes throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.readBytes(-1), throwsA(isA())); }); test('readString throws when requested length exceeds available', () { final buffer = Uint8List.fromList([0x48, 0x65, 0x6C]); // "Hel" - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.readString(10), throwsA(isA())); }); test('multiple reads exceed buffer size', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer) + final reader = FastBinaryReader(buffer) ..readUint8() // 1 byte read, 3 remaining ..readUint8() // 1 byte read, 2 remaining ..readUint16(); // 2 bytes read, 0 remaining @@ -530,21 +529,21 @@ void main() { test('peekBytes throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.peekBytes(-1), throwsA(isA())); }); test('skip throws when length exceeds available bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.skip(5), throwsA(isA())); }); test('skip throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(() => reader.skip(-1), throwsA(isA())); }); @@ -553,7 +552,7 @@ void main() { group('offset getter', () { test('offset returns current reading position', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.offset, equals(0)); @@ -567,29 +566,22 @@ void main() { expect(reader.offset, equals(4)); }); - test('offset equals usedBytes', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); - expect(reader.offset, equals(reader.usedBytes)); - - reader.readUint8(); - expect(reader.offset, equals(reader.usedBytes)); - }); - test('offset resets to 0 after reset', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); + final reader = FastBinaryReader(buffer)..readUint8(); expect(reader.offset, equals(1)); + expect(reader.availableBytes, equals(2)); reader.reset(); expect(reader.offset, equals(0)); + expect(reader.availableBytes, equals(3)); }); }); group('Special values and edge cases', () { test('readString with empty UTF-8 string', () { final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readString(0), equals('')); expect(reader.availableBytes, equals(0)); @@ -599,7 +591,7 @@ void main() { const str = '🚀👨‍👩‍👧‍👦'; // Rocket and family emoji final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); expect(reader.availableBytes, equals(0)); @@ -608,7 +600,7 @@ void main() { test('readFloat32 with NaN', () { final buffer = Uint8List(4); ByteData.view(buffer.buffer).setFloat32(0, double.nan); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat32().isNaN, isTrue); }); @@ -616,7 +608,7 @@ void main() { test('readFloat32 with Infinity', () { final buffer = Uint8List(4); ByteData.view(buffer.buffer).setFloat32(0, double.infinity); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat32(), equals(double.infinity)); }); @@ -624,7 +616,7 @@ void main() { test('readFloat32 with negative Infinity', () { final buffer = Uint8List(4); ByteData.view(buffer.buffer).setFloat32(0, double.negativeInfinity); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat32(), equals(double.negativeInfinity)); }); @@ -632,7 +624,7 @@ void main() { test('readFloat64 with NaN', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, double.nan); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat64().isNaN, isTrue); }); @@ -640,7 +632,7 @@ void main() { test('readFloat64 with Infinity', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, double.infinity); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat64(), equals(double.infinity)); }); @@ -648,7 +640,7 @@ void main() { test('readFloat64 with negative Infinity', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, double.negativeInfinity); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat64(), equals(double.negativeInfinity)); }); @@ -656,7 +648,7 @@ void main() { test('readFloat64 with negative zero', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, -0); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final value = reader.readFloat64(); expect(value, equals(0.0)); @@ -667,7 +659,7 @@ void main() { final buffer = Uint8List.fromList([ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); // Max Uint64 is 2^64 - 1 = 18446744073709551615 // In Dart, this wraps to -1 for signed int representation @@ -676,7 +668,7 @@ void main() { test('peekBytes with zero length', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.peekBytes(0), equals([])); expect(reader.offset, equals(0)); @@ -684,7 +676,7 @@ void main() { test('peekBytes with explicit zero offset', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer)..readUint8(); + final reader = FastBinaryReader(buffer)..readUint8(); final peeked = reader.peekBytes(2, 0); expect(peeked, equals([0x01, 0x02])); @@ -693,7 +685,7 @@ void main() { test('multiple resets in sequence', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = BinaryReader(buffer) + final reader = FastBinaryReader(buffer) ..readUint8() ..reset() ..reset() @@ -705,7 +697,7 @@ void main() { test('read after buffer exhaustion and reset', () { final buffer = Uint8List.fromList([0x42, 0x43]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8(), equals(0x42)); expect(reader.readUint8(), equals(0x43)); @@ -724,7 +716,7 @@ void main() { 0xFF, // Invalid byte 0x57, 0x6F, 0x72, 0x6C, 0x64, // "World" ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, contains('Hello')); @@ -733,7 +725,7 @@ void main() { test('readString with allowMalformed=false throws on invalid UTF-8', () { final buffer = Uint8List.fromList([0xFF, 0xFE, 0xFD]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect( () => reader.readString(buffer.length), @@ -743,7 +735,7 @@ void main() { test('readString handles truncated multi-byte sequence', () { final buffer = Uint8List.fromList([0xE0, 0xA0]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect( () => reader.readString(buffer.length), @@ -756,7 +748,7 @@ void main() { 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" 0xE0, 0xA0, // Incomplete 3-byte sequence ]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, startsWith('Hello')); @@ -766,7 +758,7 @@ void main() { group('Lone surrogate pairs', () { test('readString handles lone high surrogate', () { final buffer = utf8.encode('Test\uD800End'); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, isNotEmpty); @@ -774,7 +766,7 @@ void main() { test('readString handles lone low surrogate', () { final buffer = utf8.encode('Test\uDC00End'); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, isNotEmpty); @@ -786,7 +778,7 @@ void main() { 'peekBytes with offset beyond current position but within buffer', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - final reader = BinaryReader(buffer) + final reader = FastBinaryReader(buffer) ..readUint8() ..readUint8(); @@ -798,7 +790,7 @@ void main() { test('peekBytes at buffer boundary', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final peeked = reader.peekBytes(2, 3); expect(peeked, equals([4, 5])); @@ -807,7 +799,7 @@ void main() { test('peekBytes exactly at end with zero length', () { final buffer = Uint8List.fromList([1, 2, 3]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final peeked = reader.peekBytes(0, 3); expect(peeked, isEmpty); @@ -818,7 +810,7 @@ void main() { group('Sequential operations', () { test('multiple reset calls with intermediate reads', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8(), equals(1)); reader.reset(); @@ -831,7 +823,7 @@ void main() { test('alternating read and peek operations', () { final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint8(), equals(10)); expect(reader.peekBytes(2), equals([20, 30])); @@ -849,7 +841,7 @@ void main() { buffer[i] = i % 256; } - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final result = reader.readBytes(largeSize); expect(result.length, equals(largeSize)); @@ -858,7 +850,7 @@ void main() { test('skip large amount of data', () { final buffer = Uint8List(100000); - final reader = BinaryReader(buffer)..skip(50000); + final reader = FastBinaryReader(buffer)..skip(50000); expect(reader.offset, equals(50000)); expect(reader.availableBytes, equals(50000)); }); @@ -867,8 +859,8 @@ void main() { group('Buffer sharing', () { test('multiple readers can read same buffer concurrently', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader1 = BinaryReader(buffer); - final reader2 = BinaryReader(buffer); + final reader1 = FastBinaryReader(buffer); + final reader2 = FastBinaryReader(buffer); expect(reader1.readUint8(), equals(1)); expect(reader2.readUint8(), equals(1)); @@ -878,7 +870,7 @@ void main() { test('peekBytes returns independent views', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final peek1 = reader.peekBytes(3); final peek2 = reader.peekBytes(3); @@ -892,7 +884,7 @@ void main() { group('Zero-copy verification', () { test('readBytes returns view of original buffer', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final bytes = reader.readBytes(3); @@ -902,7 +894,7 @@ void main() { test('peekBytes returns view of original buffer', () { final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final peeked = reader.peekBytes(3); @@ -915,40 +907,40 @@ void main() { test('reading alternating big and little endian values', () { final writer = BinaryWriter() ..writeUint16(0x1234) - ..writeUint16(0x5678, Endian.little) + ..writeUint16(0x5678, .little) ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, Endian.little); + ..writeUint32(0x11223344, .little); final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(Endian.little), equals(0x5678)); + expect(reader.readUint16(.little), equals(0x5678)); expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(Endian.little), equals(0x11223344)); + expect(reader.readUint32(.little), equals(0x11223344)); }); test('float values with different endianness', () { final writer = BinaryWriter() ..writeFloat32(3.14) - ..writeFloat32(2.71, Endian.little) + ..writeFloat32(2.71, .little) ..writeFloat64(1.414) - ..writeFloat64(1.732, Endian.little); + ..writeFloat64(1.732, .little); final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(Endian.little), closeTo(2.71, 0.01)); + expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(Endian.little), closeTo(1.732, 0.001)); + expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); }); }); group('Boundary conditions at exact sizes', () { test('buffer exactly matches read size', () { final buffer = Uint8List.fromList([1, 2, 3, 4]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); final result = reader.readBytes(4); expect(result, equals([1, 2, 3, 4])); @@ -957,7 +949,7 @@ void main() { test('reading exactly to boundary multiple times', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6]); - final reader = BinaryReader(buffer); + final reader = FastBinaryReader(buffer); expect(reader.readUint16(), equals(0x0102)); expect(reader.readUint16(), equals(0x0304)); diff --git a/test/binary_writer_performance_test.dart b/test/binary_writer_performance_test.dart index 8fc2d79..d6db688 100644 --- a/test/binary_writer_performance_test.dart +++ b/test/binary_writer_performance_test.dart @@ -3,6 +3,48 @@ import 'dart:typed_data'; import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; +const longStringWithEmoji = + 'The quick brown fox 🦊 jumps over the lazy dog 🐕. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit 🔬. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua 🏋️. ' + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + 'ut aliquip ex ea commodo consequat ☕. ' + 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + 'dolore eu fugiat nulla pariatur 🌈. ' + 'Excepteur sint occaecat cupidatat non proident, ' + 'sunt in culpa qui officia deserunt mollit anim id est laborum. 🎯 ' + '🚀 TEST EXTENSION: Adding a second long paragraph to truly stress the ' + 'UTF-8 encoding logic. This includes more complex characters like the ' + 'Zodiac signs ♒️ ♓️ ♈️ ♉️ and some CJK characters like 日本語. ' + 'We also add a few more standard 4-byte emoji like a stack of money 💰, ' + 'a ghost 👻, and a classic thumbs up 👍 to ensure maximum complexity ' + 'in the string encoding process. The purpose of this extra length is to ' + 'force the `_ensureSize` method to be called multiple times and ensure ' + 'that the buffer resizing and copying overhead is measured correctly. ' + 'This paragraph is deliberately longer to ensure that the total byte ' + 'count for UTF-8 is significantly larger than the initial string length. ' + '🏁'; + +const shortString = 'Hello, World!'; + +final listUint8 = Uint8List.fromList([ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255, 0, 128, 64, // +]); + +final listUint16 = Uint16List.fromList([ + 1, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65535, // +]); + +final listUint32 = Uint32List.fromList([ + 1, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, + 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, + 2147483648, 4294967295, // +]); + +final listFloat32 = Float32List.fromList([ + 3.14, 2.71, 1.618, 0.5772, 1.4142, 0.6931, 2.3025, 1.732, 0.0, -1.0, -3.14, // +]).buffer.asUint8List(); + class BinaryWriterBenchmark extends BenchmarkBase { BinaryWriterBenchmark() : super('BinaryWriter performance test'); @@ -19,22 +61,38 @@ class BinaryWriterBenchmark extends BenchmarkBase { writer ..writeUint8(42) ..writeInt8(-42) - ..writeUint16(65535, Endian.little) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648, Endian.little) - ..writeUint64(9223372036854775807, Endian.little) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(3.14, Endian.little) - ..writeFloat64(3.141592653589793, Endian.little) - ..writeBytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255]) - ..writeString('Hello, World!') - ..writeString( - 'Some more data to increase buffer usage. ' - 'The quick brown fox jumps over the lazy dog.', - ); - - final _ = writer.takeBytes(); + ..writeUint16(65535, .little) + ..writeUint16(10) + ..writeInt16(-32768, .little) + ..writeInt16(-10) + ..writeUint32(4294967295, .little) + ..writeUint32(100) + ..writeInt32(-2147483648, .little) + ..writeInt32(-100) + ..writeUint64(9223372036854775807, .little) + ..writeUint64(1000) + ..writeInt64(-9223372036854775808, .little) + ..writeInt64(-1000) + ..writeFloat32(3.14, .little) + ..writeFloat32(2.71) + ..writeFloat64(3.141592653589793, .little) + ..writeFloat64(2.718281828459045) + ..writeBytes(listUint8) + ..writeBytes(listUint16) + ..writeBytes(listUint32) + ..writeBytes(listFloat32) + ..writeString(shortString) + ..writeString(longStringWithEmoji); + + final bytes = writer.takeBytes(); + + if (writer.bytesWritten != 0) { + throw StateError('bytesWritten should be reset to 0 after takeBytes()'); + } + + if (bytes.length != 1432) { + throw StateError('Unexpected byte length: ${bytes.length}'); + } } } @@ -45,6 +103,65 @@ class BinaryWriterBenchmark extends BenchmarkBase { } } +class FastBinaryWriterBenchmark extends BenchmarkBase { + FastBinaryWriterBenchmark() : super('FastBinaryWriter performance test'); + + late final FastBinaryWriter writer; + + @override + void setup() { + writer = FastBinaryWriter(); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer + ..writeUint8(42) + ..writeInt8(-42) + ..writeUint16(65535, .little) + ..writeUint16(10) + ..writeInt16(-32768, .little) + ..writeInt16(-10) + ..writeUint32(4294967295, .little) + ..writeUint32(100) + ..writeInt32(-2147483648, .little) + ..writeInt32(-100) + ..writeUint64(9223372036854775807, .little) + ..writeUint64(1000) + ..writeInt64(-9223372036854775808, .little) + ..writeInt64(-1000) + ..writeFloat32(3.14, .little) + ..writeFloat32(2.71) + ..writeFloat64(3.141592653589793, .little) + ..writeFloat64(2.718281828459045) + ..writeBytes(listUint8) + ..writeBytes(listUint16) + ..writeBytes(listUint32) + ..writeBytes(listFloat32) + ..writeString(shortString) + ..writeString(longStringWithEmoji); + + final bytes = writer.takeBytes(); + + if (writer.bytesWritten != 0) { + throw StateError('bytesWritten should be reset to 0 after takeBytes()'); + } + + if (bytes.length != 1432) { + throw StateError('Unexpected byte length: ${bytes.length}'); + } + } + } + + @override + void exercise() => run(); + static void main() { + FastBinaryWriterBenchmark().report(); + } +} + void main() { BinaryWriterBenchmark.main(); + FastBinaryWriterBenchmark.main(); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 0545048..29d1624 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -5,10 +5,10 @@ import 'package:test/test.dart'; void main() { group('BinaryWriter', () { - late BinaryWriter writer; + late FastBinaryWriter writer; setUp(() { - writer = BinaryWriter(); + writer = FastBinaryWriter(); }); test('should return empty list when takeBytes called on empty writer', () { @@ -31,7 +31,7 @@ void main() { }); test('should write Uint16 in little-endian format', () { - writer.writeUint16(256, Endian.little); + writer.writeUint16(256, .little); expect(writer.takeBytes(), [0, 1]); }); @@ -41,7 +41,7 @@ void main() { }); test('should write Int16 in little-endian format', () { - writer.writeInt16(-32768, Endian.little); + writer.writeInt16(-32768, .little); expect(writer.takeBytes(), [0, 128]); }); @@ -51,7 +51,7 @@ void main() { }); test('should write Uint32 in little-endian format', () { - writer.writeUint32(65536, Endian.little); + writer.writeUint32(65536, .little); expect(writer.takeBytes(), [0, 0, 1, 0]); }); @@ -61,7 +61,7 @@ void main() { }); test('should write Int32 in little-endian format', () { - writer.writeInt32(-2147483648, Endian.little); + writer.writeInt32(-2147483648, .little); expect(writer.takeBytes(), [0, 0, 0, 128]); }); @@ -71,7 +71,7 @@ void main() { }); test('should write Uint64 in little-endian format', () { - writer.writeUint64(4294967296, Endian.little); + writer.writeUint64(4294967296, .little); expect(writer.takeBytes(), [0, 0, 0, 0, 1, 0, 0, 0]); }); @@ -81,7 +81,7 @@ void main() { }); test('should write Int64 in little-endian format', () { - writer.writeInt64(-9223372036854775808, Endian.little); + writer.writeInt64(-9223372036854775808, .little); expect(writer.takeBytes(), [0, 0, 0, 0, 0, 0, 0, 128]); }); @@ -91,7 +91,7 @@ void main() { }); test('should write Float32 in little-endian format', () { - writer.writeFloat32(3.1415927, Endian.little); + writer.writeFloat32(3.1415927, .little); expect(writer.takeBytes(), [219, 15, 73, 64]); }); @@ -101,7 +101,7 @@ void main() { }); test('should write Float64 in little-endian format', () { - writer.writeFloat64(3.141592653589793, Endian.little); + writer.writeFloat64(3.141592653589793, .little); expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); }); @@ -1023,11 +1023,11 @@ void main() { test('writeUint64 with large value in little-endian', () { const largeValue = 123456789012345; // Safe for JS: < 2^53 - writer.writeUint64(largeValue, Endian.little); + writer.writeUint64(largeValue, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint64(Endian.little), equals(largeValue)); + expect(reader.readUint64(.little), equals(largeValue)); }); }); @@ -1179,12 +1179,12 @@ void main() { ..writeUint8(255) ..writeInt8(-128) ..writeUint16(65535) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) + ..writeInt16(-32768, .little) + ..writeUint32(4294967295, .little) ..writeInt32(-2147483648) ..writeUint64(9223372036854775807) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(3.14159, Endian.little) + ..writeInt64(-9223372036854775808, .little) + ..writeFloat32(3.14159, .little) ..writeFloat64(2.718281828) ..writeString('Hello, 世界! 🌍') ..writeBytes([1, 2, 3, 4, 5]); @@ -1195,12 +1195,12 @@ void main() { expect(reader.readUint8(), equals(255)); expect(reader.readInt8(), equals(-128)); expect(reader.readUint16(), equals(65535)); - expect(reader.readInt16(Endian.little), equals(-32768)); - expect(reader.readUint32(Endian.little), equals(4294967295)); + expect(reader.readInt16(.little), equals(-32768)); + expect(reader.readUint32(.little), equals(4294967295)); expect(reader.readInt32(), equals(-2147483648)); expect(reader.readUint64(), equals(9223372036854775807)); - expect(reader.readInt64(Endian.little), equals(-9223372036854775808)); - expect(reader.readFloat32(Endian.little), closeTo(3.14159, 0.00001)); + expect(reader.readInt64(.little), equals(-9223372036854775808)); + expect(reader.readFloat32(.little), closeTo(3.14159, 0.00001)); expect(reader.readFloat64(), closeTo(2.718281828, 0.000000001)); reader.skip(reader.availableBytes - 5); diff --git a/test/integration_test.dart b/test/integration_test.dart index 5644cd1..bfdbb38 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -45,11 +45,11 @@ void main() { final writer = BinaryWriter(); const value = 65535; - writer.writeUint16(value, Endian.little); + writer.writeUint16(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint16(Endian.little), equals(value)); + expect(reader.readUint16(.little), equals(value)); }); test('write and read Int16 with big-endian', () { @@ -67,11 +67,11 @@ void main() { final writer = BinaryWriter(); const value = -32768; - writer.writeInt16(value, Endian.little); + writer.writeInt16(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readInt16(Endian.little), equals(value)); + expect(reader.readInt16(.little), equals(value)); }); test('write and read Uint32 with big-endian', () { @@ -89,11 +89,11 @@ void main() { final writer = BinaryWriter(); const value = 4294967295; - writer.writeUint32(value, Endian.little); + writer.writeUint32(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint32(Endian.little), equals(value)); + expect(reader.readUint32(.little), equals(value)); }); test('write and read Int32 with big-endian', () { @@ -111,11 +111,11 @@ void main() { final writer = BinaryWriter(); const value = -2147483648; - writer.writeInt32(value, Endian.little); + writer.writeInt32(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readInt32(Endian.little), equals(value)); + expect(reader.readInt32(.little), equals(value)); }); test('write and read Uint64 with big-endian', () { @@ -133,11 +133,11 @@ void main() { final writer = BinaryWriter(); const value = 9223372036854775807; - writer.writeUint64(value, Endian.little); + writer.writeUint64(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readUint64(Endian.little), equals(value)); + expect(reader.readUint64(.little), equals(value)); }); test('write and read Int64 with big-endian', () { @@ -155,11 +155,11 @@ void main() { final writer = BinaryWriter(); const value = -9223372036854775808; - writer.writeInt64(value, Endian.little); + writer.writeInt64(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readInt64(Endian.little), equals(value)); + expect(reader.readInt64(.little), equals(value)); }); test('write and read Float32 with big-endian', () { @@ -177,11 +177,11 @@ void main() { final writer = BinaryWriter(); const value = 3.14159; - writer.writeFloat32(value, Endian.little); + writer.writeFloat32(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); - expect(reader.readFloat32(Endian.little), closeTo(value, 0.00001)); + expect(reader.readFloat32(.little), closeTo(value, 0.00001)); }); test('write and read Float64 with big-endian', () { @@ -199,12 +199,12 @@ void main() { final writer = BinaryWriter(); const value = 3.141592653589793; - writer.writeFloat64(value, Endian.little); + writer.writeFloat64(value, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); expect( - reader.readFloat64(Endian.little), + reader.readFloat64(.little), closeTo(value, 0.000000000000001), ); }); @@ -243,25 +243,25 @@ void main() { test('write and read with mixed endianness', () { final writer = BinaryWriter() ..writeUint16(0x1234) - ..writeUint16(0x5678, Endian.little) + ..writeUint16(0x5678, .little) ..writeUint32(0x9ABCDEF0) - ..writeUint32(0x11223344, Endian.little) + ..writeUint32(0x11223344, .little) ..writeFloat32(3.14) - ..writeFloat32(2.71, Endian.little) + ..writeFloat32(2.71, .little) ..writeFloat64(1.414) - ..writeFloat64(1.732, Endian.little); + ..writeFloat64(1.732, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); expect(reader.readUint16(), equals(0x1234)); - expect(reader.readUint16(Endian.little), equals(0x5678)); + expect(reader.readUint16(.little), equals(0x5678)); expect(reader.readUint32(), equals(0x9ABCDEF0)); - expect(reader.readUint32(Endian.little), equals(0x11223344)); + expect(reader.readUint32(.little), equals(0x11223344)); expect(reader.readFloat32(), closeTo(3.14, 0.01)); - expect(reader.readFloat32(Endian.little), closeTo(2.71, 0.01)); + expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); expect(reader.readFloat64(), closeTo(1.414, 0.001)); - expect(reader.readFloat64(Endian.little), closeTo(1.732, 0.001)); + expect(reader.readFloat64(.little), closeTo(1.732, 0.001)); }); test('write and read bytes array', () { @@ -763,48 +763,48 @@ void main() { 'all types round-trip correctly with little-endian', () { final writer = BinaryWriter() - ..writeUint16(65535, Endian.little) - ..writeInt16(-32768, Endian.little) - ..writeUint32(4294967295, Endian.little) - ..writeInt32(-2147483648, Endian.little) - ..writeUint64(9223372036854775807, Endian.little) - ..writeInt64(-9223372036854775808, Endian.little) - ..writeFloat32(1.23456, Endian.little) - ..writeFloat64(1.2345678901234, Endian.little); + ..writeUint16(65535, .little) + ..writeInt16(-32768, .little) + ..writeUint32(4294967295, .little) + ..writeInt32(-2147483648, .little) + ..writeUint64(9223372036854775807, .little) + ..writeInt64(-9223372036854775808, .little) + ..writeFloat32(1.23456, .little) + ..writeFloat64(1.2345678901234, .little); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); expect( - reader.readUint16(Endian.little), + reader.readUint16(.little), equals(65535), ); expect( - reader.readInt16(Endian.little), + reader.readInt16(.little), equals(-32768), ); expect( - reader.readUint32(Endian.little), + reader.readUint32(.little), equals(4294967295), ); expect( - reader.readInt32(Endian.little), + reader.readInt32(.little), equals(-2147483648), ); expect( - reader.readUint64(Endian.little), + reader.readUint64(.little), equals(9223372036854775807), ); expect( - reader.readInt64(Endian.little), + reader.readInt64(.little), equals(-9223372036854775808), ); expect( - reader.readFloat32(Endian.little), + reader.readFloat32(.little), closeTo(1.23456, 0.00001), ); expect( - reader.readFloat64(Endian.little), + reader.readFloat64(.little), closeTo(1.2345678901234, 0.0000001), ); expect(reader.availableBytes, equals(0)); From 309f054d6f3774501abb18d4b35974a2475c28db Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 22 Dec 2025 18:13:40 +0200 Subject: [PATCH 03/29] wip --- lib/src/binary_writer.dart | 2 +- lib/src/fast_binary_reader.dart | 128 +++++---- lib/src/fast_binary_writer.dart | 349 +++++++++++------------ test/binary_reader_performance_test.dart | 57 ++++ test/binary_reader_test.dart | 304 ++++++++++++++++++++ test/binary_writer_test.dart | 75 +++++ 6 files changed, 688 insertions(+), 227 deletions(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 0a9f0c5..cca0ce2 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -190,7 +190,7 @@ class BinaryWriter implements BinaryWriterInterface { // Instance-level temporary buffers for float conversion (thread-safe) final _tempU8 = Uint8List(8); final _tempU4 = Uint8List(4); - + late final _tempF32 = Float32List.view(_tempU4.buffer); late final _tempF64 = Float64List.view(_tempU8.buffer); diff --git a/lib/src/fast_binary_reader.dart b/lib/src/fast_binary_reader.dart index a713419..d997d28 100644 --- a/lib/src/fast_binary_reader.dart +++ b/lib/src/fast_binary_reader.dart @@ -1,15 +1,12 @@ import 'dart:convert'; import 'dart:typed_data'; -extension type const FastBinaryReader._(_Buffer _ctx) { - FastBinaryReader(Uint8List buffer) : this._(_Buffer(buffer)); +extension type const FastBinaryReader._(_Reader _ctx) { + FastBinaryReader(Uint8List buffer) : this._(_Reader(buffer)); @pragma('vm:prefer-inline') int get availableBytes => _ctx.length - _ctx.offset; - @pragma('vm:prefer-inline') - int get lengthInBytes => _ctx.lengthInBytes; - @pragma('vm:prefer-inline') int get offset => _ctx.offset; @@ -17,45 +14,66 @@ extension type const FastBinaryReader._(_Buffer _ctx) { int get length => _ctx.length; @pragma('vm:prefer-inline') - int get _offset => _ctx.offset; - - @pragma('vm:prefer-inline') - set _offset(int value) { - _ctx.offset = value; + void _checkBounds(int bytes, String type, [int? offset]) { + assert( + (offset ?? _ctx.offset) + bytes <= _ctx.length, + 'Not enough bytes to read $type: required $bytes bytes, available ' + '${_ctx.length - _ctx.offset} bytes at offset ${_ctx.offset}', + ); } @pragma('vm:prefer-inline') - ByteData get _data => _ctx.data; + int readVarInt() { + var result = 0; + var shift = 0; + + final list = _ctx.list; + var offset = _ctx.offset; + + for (var i = 0; i < 10; i++) { + assert(offset < _ctx.length, 'VarInt out of bounds'); + final byte = list[offset++]; + + result |= (byte & 0x7f) << shift; + + if ((byte & 0x80) == 0) { + _ctx.offset = offset; + return result; + } + + shift += 7; + } + + throw const FormatException('VarInt is too long (more than 10 bytes)'); + } @pragma('vm:prefer-inline') - void _checkBounds(int bytes, String type, [int? offset]) { - assert( - (offset ?? _offset) + bytes <= _ctx.length, - 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_ctx.length - _offset} bytes at offset $_offset', - ); + int readZigZag() { + final v = readVarInt(); + // Decode zig-zag encoding + return (v >>> 1) ^ -(v & 1); } @pragma('vm:prefer-inline') int readUint8() { _checkBounds(1, 'Uint8'); - return _data.getUint8(_offset++); + return _ctx.data.getUint8(_ctx.offset++); } @pragma('vm:prefer-inline') int readInt8() { _checkBounds(1, 'Int8'); - return _data.getInt8(_offset++); + return _ctx.data.getInt8(_ctx.offset++); } @pragma('vm:prefer-inline') int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); - final value = _data.getUint16(_offset, endian); - _offset += 2; + final value = _ctx.data.getUint16(_ctx.offset, endian); + _ctx.offset += 2; return value; } @@ -64,8 +82,8 @@ extension type const FastBinaryReader._(_Buffer _ctx) { int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); - final value = _data.getInt16(_offset, endian); - _offset += 2; + final value = _ctx.data.getInt16(_ctx.offset, endian); + _ctx.offset += 2; return value; } @@ -74,32 +92,32 @@ extension type const FastBinaryReader._(_Buffer _ctx) { int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); - final value = _data.getUint32(_offset, endian); - _offset += 4; + final value = _ctx.data.getUint32(_ctx.offset, endian); + _ctx.offset += 4; return value; } @pragma('vm:prefer-inline') int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); - final value = _data.getInt32(_offset, endian); - _offset += 4; + final value = _ctx.data.getInt32(_ctx.offset, endian); + _ctx.offset += 4; return value; } @pragma('vm:prefer-inline') int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); - final value = _data.getUint64(_offset, endian); - _offset += 8; + final value = _ctx.data.getUint64(_ctx.offset, endian); + _ctx.offset += 8; return value; } @pragma('vm:prefer-inline') int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); - final value = _data.getInt64(_offset, endian); - _offset += 8; + final value = _ctx.data.getInt64(_ctx.offset, endian); + _ctx.offset += 8; return value; } @@ -107,8 +125,8 @@ extension type const FastBinaryReader._(_Buffer _ctx) { double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); - final value = _data.getFloat32(_offset, endian); - _offset += 4; + final value = _ctx.data.getFloat32(_ctx.offset, endian); + _ctx.offset += 4; return value; } @@ -117,8 +135,8 @@ extension type const FastBinaryReader._(_Buffer _ctx) { double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); - final value = _data.getFloat64(_offset, endian); - _offset += 8; + final value = _ctx.data.getFloat64(_ctx.offset, endian); + _ctx.offset += 8; return value; } @@ -127,8 +145,11 @@ extension type const FastBinaryReader._(_Buffer _ctx) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Bytes'); - final bytes = _data.buffer.asUint8List(_offset, length); - _offset += length; + // Create a view of the underlying buffer without copying. + final bOffset = _ctx.baseOffset; + final bytes = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); + + _ctx.offset += length; return bytes; } @@ -141,12 +162,12 @@ extension type const FastBinaryReader._(_Buffer _ctx) { _checkBounds(length, 'String'); - final view = _data.buffer.asUint8List(_offset, length); - _offset += length; + final bOffset = _ctx.baseOffset; + final view = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); + _ctx.offset += length; return utf8.decode(view, allowMalformed: allowMalformed); } - @pragma('vm:prefer-inline') Uint8List peekBytes(int length, [int? offset]) { @@ -156,40 +177,49 @@ extension type const FastBinaryReader._(_Buffer _ctx) { return Uint8List(0); } - final peekOffset = offset ?? _offset; + final peekOffset = offset ?? _ctx.offset; _checkBounds(length, 'Peek Bytes', peekOffset); - return _data.buffer.asUint8List(peekOffset, length); + final bOffset = _ctx.baseOffset; + + return _ctx.data.buffer.asUint8List(bOffset + peekOffset, length); } void skip(int length) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Skip'); - _offset += length; + _ctx.offset += length; } @pragma('vm:prefer-inline') void reset() { - _offset = 0; + _ctx.offset = 0; } } -final class _Buffer { - _Buffer(Uint8List buffer) - : data = ByteData.sublistView(buffer).asUnmodifiableView(), +final class _Reader { + _Reader(Uint8List buffer) + : list = buffer, + data = ByteData.sublistView(buffer).asUnmodifiableView(), + buffer = buffer.buffer, length = buffer.length, - lengthInBytes = buffer.lengthInBytes, + baseOffset = buffer.offsetInBytes, offset = 0; + final Uint8List list; + /// Efficient view for typed data access. final ByteData data; + final ByteBuffer buffer; + /// Total length of the buffer. final int length; /// Current read position in the buffer. late int offset; - final int lengthInBytes; + + final int baseOffset; } diff --git a/lib/src/fast_binary_writer.dart b/lib/src/fast_binary_writer.dart index cc4daf1..eab1198 100644 --- a/lib/src/fast_binary_writer.dart +++ b/lib/src/fast_binary_writer.dart @@ -1,8 +1,8 @@ import 'dart:typed_data'; -extension type FastBinaryWriter._(_Buffer _ctx) { +extension type FastBinaryWriter._(_Writer _ctx) { FastBinaryWriter({int initialBufferSize = 128}) - : this._(_Buffer(initialBufferSize)); + : this._(_Writer(initialBufferSize)); int get bytesWritten => _ctx.offset; @@ -13,180 +13,127 @@ extension type FastBinaryWriter._(_Buffer _ctx) { } } + @pragma('vm:prefer-inline') + void writeVarInt(int value) { + // Fast path for single-byte VarInt + if (value < 0x80 && value >= 0) { + _ctx.ensureOneByte(); + _ctx.list[_ctx.offset++] = value; + return; + } + + _ctx.ensureSize(10); + + var v = value; + final list = _ctx.list; + var offset = _ctx.offset; + + while (v >= 0x80) { + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + } + + list[offset++] = v & 0x7F; + _ctx.offset = offset; + } + + void writeZigZag(int value) { + // Encode zig-zag encoding + final encoded = (value << 1) ^ (value >> 63); + writeVarInt(encoded); + } + @pragma('vm:prefer-inline') void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); - _ctx._ensureSize(1); + _ctx.ensureOneByte(); + _ctx.list[_ctx.offset++] = value; } @pragma('vm:prefer-inline') void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); - _ctx._ensureSize(1); + _ctx.ensureOneByte(); + _ctx.list[_ctx.offset++] = value & 0xFF; } @pragma('vm:prefer-inline') void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); - _ctx._ensureSize(2); + _ctx.ensureTwoBytes(); - final list = _ctx.list; - var offset = _ctx.offset; - if (endian == .big) { - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = value & 0xFF; - } else { - list[offset++] = value & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - } - _ctx.offset = offset; + _ctx.data.setUint16(_ctx.offset, value, endian); + _ctx.offset += 2; } @pragma('vm:prefer-inline') void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); - _ctx._ensureSize(2); + _ctx.ensureTwoBytes(); - final list = _ctx.list; - var offset = _ctx.offset; - if (endian == .big) { - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = value & 0xFF; - } else { - list[offset++] = value & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - } - _ctx.offset = offset; + _ctx.data.setInt16(_ctx.offset, value, endian); + _ctx.offset += 2; } @pragma('vm:prefer-inline') void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); - _ctx._ensureSize(4); + _ctx.ensureFourBytes(); - final list = _ctx.list; - var offset = _ctx.offset; - if (endian == .big) { - list[offset++] = (value >> 24) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = value & 0xFF; - } else { - list[offset++] = value & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 24) & 0xFF; - } - _ctx.offset = offset; + _ctx.data.setUint32(_ctx.offset, value, endian); + _ctx.offset += 4; } @pragma('vm:prefer-inline') void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); - _ctx._ensureSize(4); + _ctx.ensureFourBytes(); - final list = _ctx.list; - var offset = _ctx.offset; - if (endian == .big) { - list[offset++] = (value >> 24) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = value & 0xFF; - } else { - list[offset++] = value & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 24) & 0xFF; - } - _ctx.offset = offset; + _ctx.data.setInt32(_ctx.offset, value, endian); + _ctx.offset += 4; } @pragma('vm:prefer-inline') void writeUint64(int value, [Endian endian = .big]) { _checkRange(value, 0, 9223372036854775807, 'Uint64'); - _ctx._ensureSize(8); + _ctx.ensureEightBytes(); - final list = _ctx.list; - var offset = _ctx.offset; - if (endian == .big) { - list[offset++] = (value >> 56) & 0xFF; - list[offset++] = (value >> 48) & 0xFF; - list[offset++] = (value >> 40) & 0xFF; - list[offset++] = (value >> 32) & 0xFF; - list[offset++] = (value >> 24) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = value & 0xFF; - } else { - list[offset++] = value & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 24) & 0xFF; - list[offset++] = (value >> 32) & 0xFF; - list[offset++] = (value >> 40) & 0xFF; - list[offset++] = (value >> 48) & 0xFF; - list[offset++] = (value >> 56) & 0xFF; - } - _ctx.offset = offset; + _ctx.data.setUint64(_ctx.offset, value, endian); + _ctx.offset += 8; } @pragma('vm:prefer-inline') void writeInt64(int value, [Endian endian = .big]) { _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); - _ctx._ensureSize(8); + _ctx.ensureEightBytes(); - final list = _ctx.list; - var offset = _ctx.offset; - if (endian == .big) { - list[offset++] = (value >> 56) & 0xFF; - list[offset++] = (value >> 48) & 0xFF; - list[offset++] = (value >> 40) & 0xFF; - list[offset++] = (value >> 32) & 0xFF; - list[offset++] = (value >> 24) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = value & 0xFF; - } else { - list[offset++] = value & 0xFF; - list[offset++] = (value >> 8) & 0xFF; - list[offset++] = (value >> 16) & 0xFF; - list[offset++] = (value >> 24) & 0xFF; - list[offset++] = (value >> 32) & 0xFF; - list[offset++] = (value >> 40) & 0xFF; - list[offset++] = (value >> 48) & 0xFF; - list[offset++] = (value >> 56) & 0xFF; - } - _ctx.offset = offset; + _ctx.data.setInt64(_ctx.offset, value, endian); + _ctx.offset += 8; } @pragma('vm:prefer-inline') void writeFloat32(double value, [Endian endian = .big]) { - _ctx._ensureSize(4); + _ctx.ensureFourBytes(); _ctx.data.setFloat32(_ctx.offset, value, endian); _ctx.offset += 4; } @pragma('vm:prefer-inline') void writeFloat64(double value, [Endian endian = .big]) { - _ctx._ensureSize(8); + _ctx.ensureEightBytes(); _ctx.data.setFloat64(_ctx.offset, value, endian); _ctx.offset += 8; } @pragma('vm:prefer-inline') - void writeBytes(Iterable bytes) { - if (bytes.isEmpty) { - return; - } - - final length = bytes.length; - _ctx._ensureSize(length); + void writeBytes(List bytes, [int offset = 0, int? length]) { + final len = length ?? (bytes.length - offset); + _ctx.ensureSize(len); - final offset = _ctx.offset; - _ctx.list.setRange(offset, offset + length, bytes); - _ctx.offset = offset + length; + _ctx.list.setRange(_ctx.offset, _ctx.offset + len, bytes, offset); + _ctx.offset += len; } @pragma('vm:prefer-inline') @@ -196,32 +143,49 @@ extension type FastBinaryWriter._(_Buffer _ctx) { return; } - // Optimize allocation: 3 bytes per char is enough for worst-case UTF-16 - // to UTF-8 expansion.(Surrogate pairs take 2 chars for 4 bytes = 2 - // bytes/char avg. Asian chars take 1 char for 3 bytes = 3 bytes/char avg). - _ctx._ensureSize(len * 3); + // Pre-allocate: worst case for UTF-16 to UTF-8 is 3 bytes per code unit. + // (Surrogate pairs are 2 units -> 4 bytes, which is 2 bytes/unit). + _ctx.ensureSize(len * 3); final list = _ctx.list; var offset = _ctx.offset; var i = 0; while (i < len) { - // ------------------------------------------------------- - // ASCII Fast Path - // Loops tightly as long as characters are standard ASCII - // ------------------------------------------------------- var c = value.codeUnitAt(i); - if (c < 128) { - // Unroll loop slightly or trust JIT/AOT to inline checking + + if (c < 0x80) { + // ------------------------------------------------------- + // ASCII Fast Path + // ------------------------------------------------------- list[offset++] = c; i++; - // Inner loop for runs of ASCII characters + + // Unrolled loop for blocks of 4 ASCII characters + while (i <= len - 4) { + final c0 = value.codeUnitAt(i); + final c1 = value.codeUnitAt(i + 1); + final c2 = value.codeUnitAt(i + 2); + final c3 = value.codeUnitAt(i + 3); + + if ((c0 | c1 | c2 | c3) < 0x80) { + list[offset] = c0; + list[offset + 1] = c1; + list[offset + 2] = c2; + list[offset + 3] = c3; + offset += 4; + i += 4; + } else { + break; + } + } + + // Catch remaining ASCII characters before multi-byte logic while (i < len) { c = value.codeUnitAt(i); - if (c >= 128) { + if (c >= 0x80) { break; } - list[offset++] = c; i++; } @@ -234,49 +198,34 @@ extension type FastBinaryWriter._(_Buffer _ctx) { // ------------------------------------------------------- // Multi-byte handling // ------------------------------------------------------- - if (c < 2048) { - // 2 bytes (Cyrillic, extended Latin, etc.) - list[offset++] = 192 | (c >> 6); - list[offset++] = 128 | (c & 63); + if (c < 0x800) { + // 2 bytes: Cyrillic, Greek, Arabic, etc. + list[offset++] = 0xC0 | (c >> 6); + list[offset++] = 0x80 | (c & 0x3F); i++; } else if (c < 0xD800 || c > 0xDFFF) { - // 3 bytes (Standard BMP plane, excluding surrogates) - list[offset++] = 224 | (c >> 12); - list[offset++] = 128 | ((c >> 6) & 63); - list[offset++] = 128 | (c & 63); + // 3 bytes: Basic Multilingual Plane + list[offset++] = 0xE0 | (c >> 12); + list[offset++] = 0x80 | ((c >> 6) & 0x3F); + list[offset++] = 0x80 | (c & 0x3F); i++; - } else { - // 4 bytes or malformed (Surrogates) - // Check for high surrogate - if (c >= 0xD800 && c <= 0xDBFF) { - if (i + 1 < len) { - final next = value.codeUnitAt(i + 1); - if (next >= 0xDC00 && next <= 0xDFFF) { - // Valid surrogate pair - final n = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); - list[offset++] = 240 | (n >> 18); - list[offset++] = 128 | ((n >> 12) & 63); - list[offset++] = 128 | ((n >> 6) & 63); - list[offset++] = 128 | (n & 63); - i += 2; - continue; - } - } - } - - // Handle error cases (Lone surrogates) - if (!allowMalformed) { - throw FormatException( - 'Invalid UTF-16: lone surrogate at index $i', - value, - i, - ); + } else if (c <= 0xDBFF && i + 1 < len) { + // 4 bytes: Valid Surrogate Pair + final next = value.codeUnitAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + final codePoint = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); + list[offset++] = 0xF0 | (codePoint >> 18); + list[offset++] = 0x80 | ((codePoint >> 12) & 0x3F); + list[offset++] = 0x80 | ((codePoint >> 6) & 0x3F); + list[offset++] = 0x80 | (codePoint & 0x3F); + i += 2; + } else { + offset = _handleMalformed(value, i, offset, allowMalformed); + i++; } - - // Replacement char U+FFFD (EF BF BD) - list[offset++] = 0xEF; - list[offset++] = 0xBF; - list[offset++] = 0xBD; + } else { + // Malformed: Lone surrogate or end of string + offset = _handleMalformed(value, i, offset, allowMalformed); i++; } } @@ -284,6 +233,18 @@ extension type FastBinaryWriter._(_Buffer _ctx) { _ctx.offset = offset; } + @pragma('vm:prefer-inline') + int _handleMalformed(String v, int i, int offset, bool allow) { + if (!allow) { + throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); + } + final list = _ctx.list; + list[offset] = 0xEF; + list[offset + 1] = 0xBF; + list[offset + 2] = 0xBD; + return offset + 3; + } + @pragma('vm:prefer-inline') Uint8List takeBytes() { final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); @@ -298,8 +259,8 @@ extension type FastBinaryWriter._(_Buffer _ctx) { void reset() => _ctx._initializeBuffer(); } -final class _Buffer { - _Buffer(int initialBufferSize) +final class _Writer { + _Writer(int initialBufferSize) : _size = initialBufferSize, capacity = initialBufferSize, offset = 0, @@ -324,34 +285,68 @@ final class _Buffer { @pragma('vm:prefer-inline') void _initializeBuffer() { - final newBuffer = Uint8List(_size); - - list = newBuffer; + list = Uint8List(_size); + data = list.buffer.asByteData(); capacity = _size; offset = 0; } @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void _ensureSize(int size) { + void ensureSize(int size) { if (offset + size <= capacity) { return; } - + _expand(size); } + @pragma('vm:prefer-inline') + void ensureOneByte() { + if (offset + 1 <= capacity) { + return; + } + + _expand(1); + } + + @pragma('vm:prefer-inline') + void ensureTwoBytes() { + if (offset + 2 <= capacity) { + return; + } + + _expand(2); + } + + @pragma('vm:prefer-inline') + void ensureFourBytes() { + if (offset + 4 <= capacity) { + return; + } + + _expand(4); + } + + @pragma('vm:prefer-inline') + void ensureEightBytes() { + if (offset + 8 <= capacity) { + return; + } + + _expand(8); + } + void _expand(int size) { final req = offset + size; - var newCapacity = capacity * 3 ~/ 2; + var newCapacity = capacity * 2; if (newCapacity < req) { newCapacity = req; } - final list = Uint8List(newCapacity)..setRange(0, offset, this.list); + list = Uint8List(newCapacity)..setRange(0, offset, list); - this.list = list; - data = list.buffer.asByteData(0, newCapacity); + data = list.buffer.asByteData(); capacity = newCapacity; } } diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart index 03fc930..c9d8209 100644 --- a/test/binary_reader_performance_test.dart +++ b/test/binary_reader_performance_test.dart @@ -139,7 +139,64 @@ class FastBinaryReaderBenchmark extends BenchmarkBase { } } +class VarIntReaderBenchmark extends BenchmarkBase { + VarIntReaderBenchmark() : super('VarIntReader performance test'); + + late final FastBinaryReader reader; + + @override + void setup() { + const string = 'Hello, World!'; + const longString = + 'Some more data to increase buffer usage. ' + 'The quick brown fox jumps over the lazy dog.'; + + final writer = FastBinaryWriter() + ..writeVarInt(1) + ..writeVarInt(300) + ..writeVarInt(70000) + ..writeVarInt(1 << 20) + ..writeVarInt(1 << 30) + ..writeInt8(string.length) + ..writeInt32(longString.length); + + final buffer = writer.takeBytes(); + reader = FastBinaryReader(buffer); + } + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final v1 = reader.readVarInt(); + final v2 = reader.readVarInt(); + final v3 = reader.readVarInt(); + final v4 = reader.readVarInt(); + final v5 = reader.readVarInt(); + final length = reader.readInt8(); + final longLength = reader.readInt32(); + assert(v1 == 1, 'Unexpected VarInt value: $v1'); + assert(v2 == 300, 'Unexpected VarInt value: $v2'); + assert(v3 == 70000, 'Unexpected VarInt value: $v3'); + assert(v4 == 1 << 20, 'Unexpected VarInt value: $v4'); + assert(v5 == 1 << 30, 'Unexpected VarInt value: $v5'); + assert(length == 13, 'Unexpected string length: $length'); + assert(longLength == 85, 'Unexpected long string length: $longLength'); + + assert(reader.availableBytes == 0, 'Not all bytes were read'); + reader.reset(); + } + } + + static void main() { + VarIntReaderBenchmark().report(); + } +} + void main() { BinaryReaderBenchmark.main(); FastBinaryReaderBenchmark.main(); + VarIntReaderBenchmark.main(); } diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 56fff59..290822c 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -210,6 +210,178 @@ void main() { expect(reader.availableBytes, equals(0)); }); + test('readVarInt single byte (0)', () { + final buffer = Uint8List.fromList([0]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(0)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt single byte (127)', () { + final buffer = Uint8List.fromList([127]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(127)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt two bytes (128)', () { + final buffer = Uint8List.fromList([0x80, 0x01]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(128)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt two bytes (300)', () { + final buffer = Uint8List.fromList([0xAC, 0x02]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(300)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt three bytes (16384)', () { + final buffer = Uint8List.fromList([0x80, 0x80, 0x01]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(16384)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt four bytes (2097151)', () { + final buffer = Uint8List.fromList([0xFF, 0xFF, 0x7F]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(2097151)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt five bytes (268435455)', () { + final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0x7F]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(268435455)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt large value', () { + final buffer = Uint8List.fromList([0x80, 0x80, 0x80, 0x80, 0x04]); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(1 << 30)); + expect(reader.availableBytes, equals(0)); + }); + + test('readVarInt roundtrip with writeVarInt', () { + final writer = FastBinaryWriter() + ..writeVarInt(0) + ..writeVarInt(1) + ..writeVarInt(127) + ..writeVarInt(128) + ..writeVarInt(300) + ..writeVarInt(70000) + ..writeVarInt(1 << 20) + ..writeVarInt(1 << 30); + + final buffer = writer.takeBytes(); + final reader = FastBinaryReader(buffer); + + expect(reader.readVarInt(), equals(0)); + expect(reader.readVarInt(), equals(1)); + expect(reader.readVarInt(), equals(127)); + expect(reader.readVarInt(), equals(128)); + expect(reader.readVarInt(), equals(300)); + expect(reader.readVarInt(), equals(70000)); + expect(reader.readVarInt(), equals(1 << 20)); + expect(reader.readVarInt(), equals(1 << 30)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for zero', () { + final buffer = Uint8List.fromList([0]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(0)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for positive value 1', () { + final buffer = Uint8List.fromList([2]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(1)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for negative value -1', () { + final buffer = Uint8List.fromList([1]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(-1)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for positive value 2', () { + final buffer = Uint8List.fromList([4]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(2)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for negative value -2', () { + final buffer = Uint8List.fromList([3]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(-2)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for large positive value', () { + final buffer = Uint8List.fromList([0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(2147483647)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag encoding for large negative value', () { + final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(-2147483648)); + expect(reader.availableBytes, equals(0)); + }); + + test('readZigZag roundtrip with writeZigZag', () { + final writer = FastBinaryWriter() + ..writeZigZag(0) + ..writeZigZag(1) + ..writeZigZag(-1) + ..writeZigZag(2) + ..writeZigZag(-2) + ..writeZigZag(100) + ..writeZigZag(-100) + ..writeZigZag(2147483647) + ..writeZigZag(-2147483648); + + final buffer = writer.takeBytes(); + final reader = FastBinaryReader(buffer); + + expect(reader.readZigZag(), equals(0)); + expect(reader.readZigZag(), equals(1)); + expect(reader.readZigZag(), equals(-1)); + expect(reader.readZigZag(), equals(2)); + expect(reader.readZigZag(), equals(-2)); + expect(reader.readZigZag(), equals(100)); + expect(reader.readZigZag(), equals(-100)); + expect(reader.readZigZag(), equals(2147483647)); + expect(reader.readZigZag(), equals(-2147483648)); + expect(reader.availableBytes, equals(0)); + }); + test('readBytes', () { final data = [0x01, 0x02, 0x03, 0x04, 0x05]; final buffer = Uint8List.fromList(data); @@ -957,5 +1129,137 @@ void main() { expect(reader.availableBytes, equals(0)); }); }); + + group('baseOffset handling', () { + test('readBytes works correctly with non-zero baseOffset', () { + // Create a larger buffer and take a sublist + // (which will have non-zero baseOffset) + final largeBuffer = Uint8List(100); + for (var i = 0; i < 100; i++) { + largeBuffer[i] = i; + } + + // Create a view starting at offset 50 + final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); + final reader = FastBinaryReader(subBuffer); + + // Read bytes and verify they match the expected values (50-59) + final bytes = reader.readBytes(5); + expect(bytes, equals([50, 51, 52, 53, 54])); + expect(reader.availableBytes, equals(5)); + }); + + test('readString works correctly with non-zero baseOffset', () { + // Create a buffer with text data + const text = 'Hello, World!'; + final encoded = utf8.encode(text); + + // Create a larger buffer and copy the text at an offset + final largeBuffer = Uint8List(100) + ..setRange(30, 30 + encoded.length, encoded); + + // Create a view of just the text portion + final subBuffer = Uint8List.sublistView( + largeBuffer, + 30, + 30 + encoded.length, + ); + final reader = FastBinaryReader(subBuffer); + + final result = reader.readString(encoded.length); + expect(result, equals(text)); + expect(reader.availableBytes, equals(0)); + }); + + test('peekBytes works correctly with non-zero baseOffset', () { + final largeBuffer = Uint8List(50); + for (var i = 0; i < 50; i++) { + largeBuffer[i] = i; + } + + // Create a view starting at offset 20 + final subBuffer = Uint8List.sublistView(largeBuffer, 20, 30); + final reader = FastBinaryReader(subBuffer); + + // Peek at bytes without consuming them + final peeked = reader.peekBytes(5); + expect(peeked, equals([20, 21, 22, 23, 24])); + expect(reader.offset, equals(0)); + + // Now read and verify + final read = reader.readBytes(5); + expect(read, equals([20, 21, 22, 23, 24])); + expect(reader.offset, equals(5)); + }); + + test('readUint16/32/64 work correctly with non-zero baseOffset', () { + final largeBuffer = Uint8List(100); + + // Write some values at offset 40 + final writer = FastBinaryWriter() + ..writeUint16(0x1234) + ..writeUint32(0x56789ABC) + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + ..writeUint64(0x0FEDCBA987654321); + + final data = writer.takeBytes(); + largeBuffer.setRange(40, 40 + data.length, data); + + // Create a view starting at offset 40 + final subBuffer = Uint8List.sublistView( + largeBuffer, + 40, + 40 + data.length, + ); + final reader = FastBinaryReader(subBuffer); + + expect(reader.readUint16(), equals(0x1234)); + expect(reader.readUint32(), equals(0x56789ABC)); + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + expect(reader.readUint64(), equals(0x0FEDCBA987654321)); + expect(reader.availableBytes, equals(0)); + }); + + test('multiple readers from different offsets', () { + final largeBuffer = Uint8List(100); + for (var i = 0; i < 100; i++) { + largeBuffer[i] = i; + } + + // Create two readers from different offsets + final reader1 = FastBinaryReader( + Uint8List.sublistView(largeBuffer, 10, 20), + ); + final reader2 = FastBinaryReader( + Uint8List.sublistView(largeBuffer, 50, 60), + ); + + expect(reader1.readUint8(), equals(10)); + expect(reader2.readUint8(), equals(50)); + + expect(reader1.readBytes(3), equals([11, 12, 13])); + expect(reader2.readBytes(3), equals([51, 52, 53])); + }); + + test('baseOffset with readString containing multi-byte UTF-8', () { + const text = 'Привет мир! 🌍'; + final encoded = utf8.encode(text); + + final largeBuffer = Uint8List(200) + ..setRange(75, 75 + encoded.length, encoded); + + final subBuffer = Uint8List.sublistView( + largeBuffer, + 75, + 75 + encoded.length, + ); + final reader = FastBinaryReader(subBuffer); + + final result = reader.readString(encoded.length); + expect(result, equals(text)); + }); + }); }); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 29d1624..11c026a 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -105,6 +105,81 @@ void main() { expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); }); + test('should write VarInt single byte (0)', () { + writer.writeVarInt(0); + expect(writer.takeBytes(), [0]); + }); + + test('should write VarInt single byte (127)', () { + writer.writeVarInt(127); + expect(writer.takeBytes(), [127]); + }); + + test('should write VarInt two bytes (128)', () { + writer.writeVarInt(128); + expect(writer.takeBytes(), [0x80, 0x01]); + }); + + test('should write VarInt two bytes (300)', () { + writer.writeVarInt(300); + expect(writer.takeBytes(), [0xAC, 0x02]); + }); + + test('should write VarInt three bytes (16384)', () { + writer.writeVarInt(16384); + expect(writer.takeBytes(), [0x80, 0x80, 0x01]); + }); + + test('should write VarInt four bytes (2097151)', () { + writer.writeVarInt(2097151); + expect(writer.takeBytes(), [0xFF, 0xFF, 0x7F]); + }); + + test('should write VarInt five bytes (268435455)', () { + writer.writeVarInt(268435455); + expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0x7F]); + }); + + test('should write VarInt large value', () { + writer.writeVarInt(1 << 30); + expect(writer.takeBytes(), [0x80, 0x80, 0x80, 0x80, 0x04]); + }); + + test('should write ZigZag encoding for positive values', () { + writer.writeZigZag(0); + expect(writer.takeBytes(), [0]); + }); + + test('should write ZigZag encoding for positive value 1', () { + writer.writeZigZag(1); + expect(writer.takeBytes(), [2]); + }); + + test('should write ZigZag encoding for negative value -1', () { + writer.writeZigZag(-1); + expect(writer.takeBytes(), [1]); + }); + + test('should write ZigZag encoding for positive value 2', () { + writer.writeZigZag(2); + expect(writer.takeBytes(), [4]); + }); + + test('should write ZigZag encoding for negative value -2', () { + writer.writeZigZag(-2); + expect(writer.takeBytes(), [3]); + }); + + test('should write ZigZag encoding for large positive value', () { + writer.writeZigZag(2147483647); + expect(writer.takeBytes(), [0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); + }); + + test('should write ZigZag encoding for large negative value', () { + writer.writeZigZag(-2147483648); + expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); + }); + test('should write byte array correctly', () { writer.writeBytes([1, 2, 3, 4, 5]); expect(writer.takeBytes(), [1, 2, 3, 4, 5]); From cdd158685af034b661a8ae7724803dced4bb1234 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 22 Dec 2025 18:18:03 +0200 Subject: [PATCH 04/29] cleanup --- lib/pro_binary.dart | 2 - lib/src/binary_reader.dart | 202 +++++---- lib/src/binary_reader_interface.dart | 268 ------------ lib/src/binary_writer.dart | 520 +++++++++++------------ lib/src/binary_writer_interface.dart | 302 ------------- lib/src/fast_binary_reader.dart | 225 ---------- lib/src/fast_binary_writer.dart | 352 --------------- test/binary_reader_performance_test.dart | 129 +----- test/binary_reader_test.dart | 240 +++++------ test/binary_writer_performance_test.dart | 59 --- test/binary_writer_test.dart | 4 +- test/integration_test.dart | 4 +- 12 files changed, 466 insertions(+), 1841 deletions(-) delete mode 100644 lib/src/binary_reader_interface.dart delete mode 100644 lib/src/binary_writer_interface.dart delete mode 100644 lib/src/fast_binary_reader.dart delete mode 100644 lib/src/fast_binary_writer.dart diff --git a/lib/pro_binary.dart b/lib/pro_binary.dart index 193b3af..ed0e0c1 100644 --- a/lib/pro_binary.dart +++ b/lib/pro_binary.dart @@ -3,5 +3,3 @@ library; export 'src/binary_reader.dart'; export 'src/binary_writer.dart'; -export 'src/fast_binary_reader.dart'; -export 'src/fast_binary_writer.dart'; diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index aa5cd50..5f86fbd 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -1,188 +1,160 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'binary_reader_interface.dart'; - -/// A high-performance implementation of [BinaryReaderInterface] for decoding -/// binary data. -/// -/// Example: -/// ```dart -/// final bytes = Uint8List.fromList([0, 0, 0, 42]); -/// final reader = BinaryReader(bytes); -/// final value = reader.readUint32(); // 42 -/// print(reader.availableBytes); // 0 -/// ``` -class BinaryReader implements BinaryReaderInterface { - /// Creates a new [BinaryReader] for the given byte buffer. - /// - /// The [buffer] parameter must be a [Uint8List] containing the data to read. - /// The reader starts at position 0 and can read up to the buffer's length. - BinaryReader(Uint8List buffer) - : _buffer = buffer, - _data = ByteData.sublistView(buffer), - _length = buffer.length; - - /// The underlying byte buffer being read from. - final Uint8List _buffer; +extension type const BinaryReader._(_Reader _ctx) { + BinaryReader(Uint8List buffer) : this._(_Reader(buffer)); - /// Efficient view for typed data access. - final ByteData _data; + @pragma('vm:prefer-inline') + int get availableBytes => _ctx.length - _ctx.offset; - /// Total length of the buffer. - final int _length; + @pragma('vm:prefer-inline') + int get offset => _ctx.offset; - /// Current read position in the buffer. - var _offset = 0; + @pragma('vm:prefer-inline') + int get length => _ctx.length; - /// Performs inline bounds check to ensure safe reads. - /// - /// Throws [AssertionError] if attempting to read beyond buffer boundaries. @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') void _checkBounds(int bytes, String type, [int? offset]) { assert( - (offset ?? _offset) + bytes <= _length, + (offset ?? _ctx.offset) + bytes <= _ctx.length, 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_length - _offset} bytes at offset $_offset', + '${_ctx.length - _ctx.offset} bytes at offset ${_ctx.offset}', ); } - @override - int get availableBytes => _length - _offset; + @pragma('vm:prefer-inline') + int readVarInt() { + var result = 0; + var shift = 0; + + final list = _ctx.list; + var offset = _ctx.offset; + + for (var i = 0; i < 10; i++) { + assert(offset < _ctx.length, 'VarInt out of bounds'); + final byte = list[offset++]; + + result |= (byte & 0x7f) << shift; - @override - int get usedBytes => _offset; + if ((byte & 0x80) == 0) { + _ctx.offset = offset; + return result; + } + + shift += 7; + } + + throw const FormatException('VarInt is too long (more than 10 bytes)'); + } + + @pragma('vm:prefer-inline') + int readZigZag() { + final v = readVarInt(); + // Decode zig-zag encoding + return (v >>> 1) ^ -(v & 1); + } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readUint8() { _checkBounds(1, 'Uint8'); - return _data.getUint8(_offset++); + + return _ctx.data.getUint8(_ctx.offset++); } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readInt8() { _checkBounds(1, 'Int8'); - return _data.getInt8(_offset++); + return _ctx.data.getInt8(_ctx.offset++); } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); - final value = _data.getUint16(_offset, endian); - _offset += 2; + final value = _ctx.data.getUint16(_ctx.offset, endian); + _ctx.offset += 2; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); - final value = _data.getInt16(_offset, endian); - _offset += 2; + final value = _ctx.data.getInt16(_ctx.offset, endian); + _ctx.offset += 2; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); - final value = _data.getUint32(_offset, endian); - _offset += 4; - + final value = _ctx.data.getUint32(_ctx.offset, endian); + _ctx.offset += 4; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); - - final value = _data.getInt32(_offset, endian); - _offset += 4; - + final value = _ctx.data.getInt32(_ctx.offset, endian); + _ctx.offset += 4; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); - - final value = _data.getUint64(_offset, endian); - _offset += 8; - + final value = _ctx.data.getUint64(_ctx.offset, endian); + _ctx.offset += 8; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); - - final value = _data.getInt64(_offset, endian); - _offset += 8; - + final value = _ctx.data.getInt64(_ctx.offset, endian); + _ctx.offset += 8; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); - final value = _data.getFloat32(_offset, endian); - _offset += 4; + final value = _ctx.data.getFloat32(_ctx.offset, endian); + _ctx.offset += 4; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); - final value = _data.getFloat64(_offset, endian); - _offset += 8; - + final value = _ctx.data.getFloat64(_ctx.offset, endian); + _ctx.offset += 8; return value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override Uint8List readBytes(int length) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Bytes'); - final bytes = Uint8List.sublistView(_buffer, _offset, _offset + length); - _offset += length; + // Create a view of the underlying buffer without copying. + final bOffset = _ctx.baseOffset; + final bytes = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); + + _ctx.offset += length; return bytes; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override String readString(int length, {bool allowMalformed = false}) { if (length == 0) { return ''; @@ -190,15 +162,14 @@ class BinaryReader implements BinaryReaderInterface { _checkBounds(length, 'String'); - final view = Uint8List.sublistView(_buffer, _offset, _offset + length); - _offset += length; + final bOffset = _ctx.baseOffset; + final view = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); + _ctx.offset += length; return utf8.decode(view, allowMalformed: allowMalformed); } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override Uint8List peekBytes(int length, [int? offset]) { assert(length >= 0, 'Length must be non-negative'); @@ -206,27 +177,48 @@ class BinaryReader implements BinaryReaderInterface { return Uint8List(0); } - final peekOffset = offset ?? _offset; + final peekOffset = offset ?? _ctx.offset; _checkBounds(length, 'Peek Bytes', peekOffset); - return Uint8List.sublistView(_buffer, peekOffset, peekOffset + length); + final bOffset = _ctx.baseOffset; + + return _ctx.data.buffer.asUint8List(bOffset + peekOffset, length); } - @override void skip(int length) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Skip'); - _offset += length; + _ctx.offset += length; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void reset() { - _offset = 0; + _ctx.offset = 0; } +} + +final class _Reader { + _Reader(Uint8List buffer) + : list = buffer, + data = ByteData.sublistView(buffer).asUnmodifiableView(), + buffer = buffer.buffer, + length = buffer.length, + baseOffset = buffer.offsetInBytes, + offset = 0; + + final Uint8List list; + + /// Efficient view for typed data access. + final ByteData data; + + final ByteBuffer buffer; + + /// Total length of the buffer. + final int length; + + /// Current read position in the buffer. + late int offset; - @override - int get offset => _offset; + final int baseOffset; } diff --git a/lib/src/binary_reader_interface.dart b/lib/src/binary_reader_interface.dart deleted file mode 100644 index 5049262..0000000 --- a/lib/src/binary_reader_interface.dart +++ /dev/null @@ -1,268 +0,0 @@ -import 'dart:typed_data'; - -/// The [BinaryReaderInterface] class is an abstract base class used to decode -/// various types of data from a binary format. -abstract interface class BinaryReaderInterface { - /// Returns the number of bytes available to read from the buffer. - /// - /// This getter calculates the difference between the total length of the - /// buffer and the current offset, indicating the remaining bytes that can - /// still be read. - int get availableBytes; - - /// Returns the number of bytes that have been read from the buffer. - /// - /// This getter returns the current offset, indicating how many bytes have - /// been consumed from the buffer since the start. - int get usedBytes; - - /// Reads an 8-bit unsigned integer from the buffer. - /// - /// This method reads an 8-bit unsigned integer from the current offset - /// position and increments the offset by 1 byte. - /// - /// Returns an unsigned 8-bit integer (range: 0 to 255). - /// - /// Example: - /// ```dart - /// int value = reader.readUint8(); // Reads a single byte as an unsigned integer. - /// ``` - int readUint8(); - - /// Reads an 8-bit signed integer from the buffer. - /// - /// This method reads an 8-bit signed integer from the current offset position - /// and increments the offset by 1 byte. - /// - /// Returns a signed 8-bit integer (range: -128 to 127). - /// - /// Example: - /// ```dart - /// int value = reader.readInt8(); // Reads a single byte as a signed integer. - /// ``` - int readInt8(); - - /// Reads a 16-bit unsigned integer from the buffer. - /// - /// This method reads a 16-bit unsigned integer from the current offset - /// position with the specified byte order (endian) and increments the offset - /// by 2 bytes. - /// - /// Returns an unsigned 16-bit integer (range: 0 to 65535). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readUint16(); // Reads two bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint16(.little); // Reads two bytes as an unsigned integer in little-endian order. - /// ``` - int readUint16([Endian endian = .big]); - - /// Reads a 16-bit signed integer from the buffer. - /// - /// This method reads a 16-bit signed integer from the current offset position - /// with the specified byte order (endian) and increments the offset by 2 - /// bytes. - /// - /// Returns a signed 16-bit integer (range: -32768 to 32767). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readInt16(); // Reads two bytes as a signed integer in big-endian order. - /// int value = reader.readInt16(.little); // Reads two bytes as a signed integer in little-endian order. - /// ``` - int readInt16([Endian endian = .big]); - - /// Reads a 32-bit unsigned integer from the buffer. - /// - /// This method reads a 32-bit unsigned integer from the current offset - /// position with the specified byte order (endian) and increments the offset - /// by 4 bytes. - /// - /// Returns an unsigned 32-bit integer (range: 0 to 4294967295). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readUint32(); // Reads four bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint32(.little); // Reads four bytes as an unsigned integer in little-endian order. - /// ``` - int readUint32([Endian endian = .big]); - - /// Reads a 32-bit signed integer from the buffer. - /// - /// This method reads a 32-bit signed integer from the current offset position - /// with the specified byte order (endian) and increments the offset by 4 - /// bytes. - /// - /// Returns a signed 32-bit integer (range: -2147483648 to 2147483647). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readInt32(); // Reads four bytes as a signed integer in big-endian order. - /// int value = reader.readInt32(.little); // Reads four bytes as a signed integer in little-endian order. - /// ``` - int readInt32([Endian endian = .big]); - - /// Reads a 64-bit unsigned integer from the buffer. - /// - /// This method reads a 64-bit unsigned integer from the current offset - /// position with the specified byte order (endian) and increments the offset - /// by 8 bytes. - /// - /// Returns an unsigned 64-bit integer (range: 0 to 18446744073709551615). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readUint64(); // Reads eight bytes as an unsigned integer in big-endian order. - /// int value = reader.readUint64(.little); // Reads eight bytes as an unsigned integer in little-endian order. - /// ``` - int readUint64([Endian endian = .big]); - - /// Reads a 64-bit signed integer from the buffer. - /// - /// This method reads a 64-bit signed integer from the current offset position - /// with the specified byte order (endian) and increments the offset by 8 - /// bytes. - /// - /// Returns a signed 64-bit integer - /// (range: -9223372036854775808 to 9223372036854775807). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// int value = reader.readInt64(); // Reads eight bytes as a signed integer in big-endian order. - /// int value = reader.readInt64(.little); // Reads eight bytes as a signed integer in little-endian order. - /// ``` - int readInt64([Endian endian = .big]); - - /// Reads a 32-bit floating point number from the buffer. - /// - /// This method reads a 32-bit float from the current offset position with the - /// specified byte order (endian) and increments the offset by 4 bytes. - /// - /// Returns a 32-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use - /// (defaults to [.big]). - /// - /// Example: - /// ```dart - /// double value = reader.readFloat32(); // Reads four bytes as a float in big-endian order. - /// double value = reader.readFloat32(.little); // Reads four bytes as a float in little-endian order. - /// ``` - double readFloat32([Endian endian = .big]); - - /// Reads a 64-bit floating point number from the buffer. - /// - /// This method reads a 64-bit float from the current offset position with the - /// specified byte order (endian) and increments the offset by 8 bytes. - /// - /// Returns a 64-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// double value = reader.readFloat64(); // Reads eight bytes as a float in big-endian order. - /// double value = reader.readFloat64(.little); // Reads eight bytes as a float in little-endian order. - /// ``` - double readFloat64([Endian endian = .big]); - - /// Reads a list of bytes from the buffer. - /// - /// This method reads the specified number of bytes from the current offset - /// position and increments the offset by that number of bytes. - /// - /// The [length] parameter specifies the number of bytes to read. - /// - /// Returns a [Uint8List] containing the read bytes. - /// - /// Example: - /// ```dart - /// Uint8List bytes = reader.readBytes(5); // Reads five bytes from the buffer. - /// ``` - Uint8List readBytes(int length); - - /// Reads a UTF-8 encoded string from the buffer. - /// - /// This method reads the specified number of bytes from the buffer, decodes - /// them using UTF-8 encoding, and returns the resulting string. The offset - /// is incremented by the length of the read bytes. - /// - /// The [length] parameter specifies the number of bytes to read from the - /// buffer. - /// - /// The optional [allowMalformed] parameter specifies whether to allow - /// malformed UTF-8 sequences (defaults to false). - /// - /// Example: - /// ```dart - /// String value = reader.readString(5); // Reads 5 bytes and decodes them as a UTF-8 string. - /// ``` - String readString(int length, {bool allowMalformed = false}); - - /// Peeks a list of bytes from the buffer without changing the internal state. - /// - /// This method reads the specified number of bytes from the specified offset - /// position and does not change the current offset. - /// - /// The [length] parameter specifies the number of bytes to read. - /// The optional [offset] parameter specifies the offset position to start - /// reading (defaults to the current offset). - /// - /// Returns a [Uint8List] containing the read bytes. - /// - /// Example: - /// ```dart - /// Uint8List bytes = reader.peekBytes(5); // Reads five bytes from the current offset without changing the offset. - /// Uint8List bytes = reader.peekBytes(5, 10); // Reads five bytes from the specified offset (10) without changing the offset. - /// ``` - Uint8List peekBytes(int length, [int? offset]); - - /// Skips the specified number of bytes in the buffer. - /// - /// This method increments the current offset by the specified number of - /// bytes, effectively skipping over that number of bytes in the buffer. - /// - /// The [length] parameter specifies the number of bytes to skip. - /// - /// Example: - /// ```dart - /// reader.skip(5); // Skips the next 5 bytes in the buffer. - /// ``` - void skip(int length); - - /// Resets the reader to the initial state. - /// - /// This method sets the current offset back to 0, allowing the reader to - /// start reading from the beginning of the buffer again. - /// - /// Example: - /// ```dart - /// reader.readUint8(); // Reads a byte - /// reader.reset(); // Resets to the beginning - /// reader.readUint8(); // Reads the same byte again - /// ``` - void reset(); - - /// Returns the current offset position in the buffer. - /// - /// This getter returns the current reading position, which is the same as - /// [usedBytes]. This is useful when you need to save the current position - /// to return to it later. - /// - /// Example: - /// ```dart - /// int position = reader.offset; // Gets current position - /// ``` - int get offset; -} diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index cca0ce2..fef3061 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,384 +1,352 @@ import 'dart:typed_data'; -import 'binary_writer_interface.dart'; - -/// A high-performance implementation of [BinaryWriterInterface] for encoding -/// data into binary format. -/// -/// Example: -/// ```dart -/// final writer = BinaryWriter(); -/// writer.writeUint32(42); -/// writer.writeString('Hello'); -/// final bytes = writer.toBytes(); // View without reset -/// writer.writeUint8(10); // Can continue writing -/// final final = writer.takeBytes(); // View with reset -/// ``` -class BinaryWriter implements BinaryWriterInterface { - /// Creates a new [BinaryWriter] with an optional initial buffer size. - /// - /// The [initialBufferSize] parameter specifies the initial capacity of the - /// internal buffer (defaults to 64 bytes). Choose a larger value if you - /// expect to write large amounts of data to reduce reallocations. - BinaryWriter({int initialBufferSize = 64}) - : _initialBufferSize = initialBufferSize { - _initializeBuffer(initialBufferSize); +extension type BinaryWriter._(_Writer _ctx) { + BinaryWriter({int initialBufferSize = 128}) + : this._(_Writer(initialBufferSize)); + + int get bytesWritten => _ctx.offset; + + @pragma('vm:prefer-inline') + void _checkRange(int value, int min, int max, String typeName) { + if (value < min || value > max) { + throw RangeError.range(value, min, max, typeName); + } } - final int _initialBufferSize; + @pragma('vm:prefer-inline') + void writeVarInt(int value) { + // Fast path for single-byte VarInt + if (value < 0x80 && value >= 0) { + _ctx.ensureOneByte(); + _ctx.list[_ctx.offset++] = value; + return; + } - /// Internal buffer for storing binary data. - late Uint8List _buffer; + _ctx.ensureSize(10); - /// Current write position in the buffer. - var _offset = 0; + var v = value; + final list = _ctx.list; + var offset = _ctx.offset; - /// Cached buffer capacity to avoid repeated length checks. - var _capacity = 0; + while (v >= 0x80) { + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + } - @override - int get bytesWritten => _offset; + list[offset++] = v & 0x7F; + _ctx.offset = offset; + } + + void writeZigZag(int value) { + // Encode zig-zag encoding + final encoded = (value << 1) ^ (value >> 63); + writeVarInt(encoded); + } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); - _ensureSize(1); + _ctx.ensureOneByte(); - _buffer[_offset++] = value; + _ctx.list[_ctx.offset++] = value; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); - _ensureSize(1); + _ctx.ensureOneByte(); - _buffer[_offset++] = value & 0xFF; + _ctx.list[_ctx.offset++] = value & 0xFF; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); - _ensureSize(2); - - if (endian == .big) { - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - } + _ctx.ensureTwoBytes(); + + _ctx.data.setUint16(_ctx.offset, value, endian); + _ctx.offset += 2; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); - _ensureSize(2); - - if (endian == .big) { - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - } + _ctx.ensureTwoBytes(); + + _ctx.data.setInt16(_ctx.offset, value, endian); + _ctx.offset += 2; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); - _ensureSize(4); - - if (endian == .big) { - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - } + _ctx.ensureFourBytes(); + + _ctx.data.setUint32(_ctx.offset, value, endian); + _ctx.offset += 4; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); - _ensureSize(4); - - if (endian == .big) { - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - } + _ctx.ensureFourBytes(); + + _ctx.data.setInt32(_ctx.offset, value, endian); + _ctx.offset += 4; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeUint64(int value, [Endian endian = .big]) { _checkRange(value, 0, 9223372036854775807, 'Uint64'); - _ensureSize(8); - - if (endian == .big) { - _buffer[_offset++] = (value >> 56) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 56) & 0xFF; - } + _ctx.ensureEightBytes(); + + _ctx.data.setUint64(_ctx.offset, value, endian); + _ctx.offset += 8; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeInt64(int value, [Endian endian = .big]) { _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); - _ensureSize(8); - - if (endian == .big) { - _buffer[_offset++] = (value >> 56) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = value & 0xFF; - } else { - _buffer[_offset++] = value & 0xFF; - _buffer[_offset++] = (value >> 8) & 0xFF; - _buffer[_offset++] = (value >> 16) & 0xFF; - _buffer[_offset++] = (value >> 24) & 0xFF; - _buffer[_offset++] = (value >> 32) & 0xFF; - _buffer[_offset++] = (value >> 40) & 0xFF; - _buffer[_offset++] = (value >> 48) & 0xFF; - _buffer[_offset++] = (value >> 56) & 0xFF; - } - } - - // Instance-level temporary buffers for float conversion (thread-safe) - final _tempU8 = Uint8List(8); - final _tempU4 = Uint8List(4); + _ctx.ensureEightBytes(); - late final _tempF32 = Float32List.view(_tempU4.buffer); - late final _tempF64 = Float64List.view(_tempU8.buffer); + _ctx.data.setInt64(_ctx.offset, value, endian); + _ctx.offset += 8; + } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeFloat32(double value, [Endian endian = .big]) { - _ensureSize(4); - _tempF32[0] = value; // Write to temp buffer - - if (endian == .big) { - _buffer[_offset++] = _tempU4[3]; - _buffer[_offset++] = _tempU4[2]; - _buffer[_offset++] = _tempU4[1]; - _buffer[_offset++] = _tempU4[0]; - } else { - _buffer.setRange(_offset, _offset + 4, _tempU4); - _offset += 4; - } + _ctx.ensureFourBytes(); + _ctx.data.setFloat32(_ctx.offset, value, endian); + _ctx.offset += 4; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeFloat64(double value, [Endian endian = .big]) { - _ensureSize(8); - _tempF64[0] = value; - if (endian == .big) { - _buffer[_offset++] = _tempU8[7]; - _buffer[_offset++] = _tempU8[6]; - _buffer[_offset++] = _tempU8[5]; - _buffer[_offset++] = _tempU8[4]; - _buffer[_offset++] = _tempU8[3]; - _buffer[_offset++] = _tempU8[2]; - _buffer[_offset++] = _tempU8[1]; - _buffer[_offset++] = _tempU8[0]; - } else { - _buffer.setRange(_offset, _offset + 8, _tempU8); - _offset += 8; - } + _ctx.ensureEightBytes(); + _ctx.data.setFloat64(_ctx.offset, value, endian); + _ctx.offset += 8; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override - void writeBytes(Iterable bytes) { - // Early return for empty byte lists - if (bytes.isEmpty) { - return; - } - - final length = bytes.length; - _ensureSize(length); + void writeBytes(List bytes, [int offset = 0, int? length]) { + final len = length ?? (bytes.length - offset); + _ctx.ensureSize(len); - _buffer.setRange(_offset, _offset + length, bytes); - _offset += length; + _ctx.list.setRange(_ctx.offset, _ctx.offset + len, bytes, offset); + _ctx.offset += len; } @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - @override void writeString(String value, {bool allowMalformed = true}) { final len = value.length; if (len == 0) { return; } - // Over-allocate max UTF-8 size (4 bytes/char) - _ensureSize(len * 4); + // Pre-allocate: worst case for UTF-16 to UTF-8 is 3 bytes per code unit. + // (Surrogate pairs are 2 units -> 4 bytes, which is 2 bytes/unit). + _ctx.ensureSize(len * 3); - var bufIdx = _offset; - for (var i = 0; i < len; i++) { + final list = _ctx.list; + var offset = _ctx.offset; + var i = 0; + + while (i < len) { var c = value.codeUnitAt(i); - if (c < 128) { - _buffer[bufIdx++] = c; - } else if (c < 2048) { - _buffer[bufIdx++] = 192 | (c >> 6); - _buffer[bufIdx++] = 128 | (c & 63); - } else if (c >= 0xD800 && c <= 0xDBFF) { - // High surrogate - if (i + 1 < len) { - final next = value.codeUnitAt(i + 1); - if (next >= 0xDC00 && next <= 0xDFFF) { - // Valid surrogate pair - i++; - c = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); - _buffer[bufIdx++] = 240 | (c >> 18); - _buffer[bufIdx++] = 128 | ((c >> 12) & 63); - _buffer[bufIdx++] = 128 | ((c >> 6) & 63); - _buffer[bufIdx++] = 128 | (c & 63); - continue; + + if (c < 0x80) { + // ------------------------------------------------------- + // ASCII Fast Path + // ------------------------------------------------------- + list[offset++] = c; + i++; + + // Unrolled loop for blocks of 4 ASCII characters + while (i <= len - 4) { + final c0 = value.codeUnitAt(i); + final c1 = value.codeUnitAt(i + 1); + final c2 = value.codeUnitAt(i + 2); + final c3 = value.codeUnitAt(i + 3); + + if ((c0 | c1 | c2 | c3) < 0x80) { + list[offset] = c0; + list[offset + 1] = c1; + list[offset + 2] = c2; + list[offset + 3] = c3; + offset += 4; + i += 4; + } else { + break; } } - // Lone high surrogate - if (!allowMalformed) { - throw FormatException( - 'Invalid UTF-16: lone high surrogate at index $i', - value, - i, - ); + + // Catch remaining ASCII characters before multi-byte logic + while (i < len) { + c = value.codeUnitAt(i); + if (c >= 0x80) { + break; + } + list[offset++] = c; + i++; } - // Replacement char U+FFFD - _buffer[bufIdx++] = 0xEF; - _buffer[bufIdx++] = 0xBF; - _buffer[bufIdx++] = 0xBD; - } else if (c >= 0xDC00 && c <= 0xDFFF) { - // Lone low surrogate - if (!allowMalformed) { - throw FormatException( - 'Invalid UTF-16: lone low surrogate at index $i', - value, - i, - ); + + if (i == len) { + break; + } + } + + // ------------------------------------------------------- + // Multi-byte handling + // ------------------------------------------------------- + if (c < 0x800) { + // 2 bytes: Cyrillic, Greek, Arabic, etc. + list[offset++] = 0xC0 | (c >> 6); + list[offset++] = 0x80 | (c & 0x3F); + i++; + } else if (c < 0xD800 || c > 0xDFFF) { + // 3 bytes: Basic Multilingual Plane + list[offset++] = 0xE0 | (c >> 12); + list[offset++] = 0x80 | ((c >> 6) & 0x3F); + list[offset++] = 0x80 | (c & 0x3F); + i++; + } else if (c <= 0xDBFF && i + 1 < len) { + // 4 bytes: Valid Surrogate Pair + final next = value.codeUnitAt(i + 1); + if (next >= 0xDC00 && next <= 0xDFFF) { + final codePoint = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); + list[offset++] = 0xF0 | (codePoint >> 18); + list[offset++] = 0x80 | ((codePoint >> 12) & 0x3F); + list[offset++] = 0x80 | ((codePoint >> 6) & 0x3F); + list[offset++] = 0x80 | (codePoint & 0x3F); + i += 2; + } else { + offset = _handleMalformed(value, i, offset, allowMalformed); + i++; } - // Replacement char U+FFFD - _buffer[bufIdx++] = 0xEF; - _buffer[bufIdx++] = 0xBF; - _buffer[bufIdx++] = 0xBD; } else { - // 3 bytes - _buffer[bufIdx++] = 224 | (c >> 12); - _buffer[bufIdx++] = 128 | ((c >> 6) & 63); - _buffer[bufIdx++] = 128 | (c & 63); + // Malformed: Lone surrogate or end of string + offset = _handleMalformed(value, i, offset, allowMalformed); + i++; } } - _offset = bufIdx; + _ctx.offset = offset; } - @override + @pragma('vm:prefer-inline') + int _handleMalformed(String v, int i, int offset, bool allow) { + if (!allow) { + throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); + } + final list = _ctx.list; + list[offset] = 0xEF; + list[offset + 1] = 0xBF; + list[offset + 2] = 0xBD; + return offset + 3; + } + + @pragma('vm:prefer-inline') Uint8List takeBytes() { - final result = Uint8List.sublistView(_buffer, 0, _offset); + final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); + _ctx._initializeBuffer(); + return result; + } - _offset = 0; - _initializeBuffer(_initialBufferSize); + @pragma('vm:prefer-inline') + Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); - return result; + @pragma('vm:prefer-inline') + void reset() => _ctx._initializeBuffer(); +} + +final class _Writer { + _Writer(int initialBufferSize) + : _size = initialBufferSize, + capacity = initialBufferSize, + offset = 0, + list = Uint8List(initialBufferSize) { + data = list.buffer.asByteData(); } - @override - Uint8List toBytes() => Uint8List.sublistView(_buffer, 0, _offset); + /// Current write position in the buffer. + late int offset; + + /// Cached buffer capacity to avoid repeated length checks. + late int capacity; + + /// Underlying byte buffer. + late Uint8List list; + + /// ByteData view of the underlying buffer for efficient writes. + late ByteData data; - @override - void reset() { - _offset = 0; - _initializeBuffer(_initialBufferSize); + /// Initial buffer size. + final int _size; + + @pragma('vm:prefer-inline') + void _initializeBuffer() { + list = Uint8List(_size); + data = list.buffer.asByteData(); + capacity = _size; + offset = 0; } - /// Initializes the buffer with the specified size. @pragma('vm:prefer-inline') @pragma('dart2js:tryInline') - void _initializeBuffer(int size) { - _buffer = Uint8List(size); - _capacity = size; + void ensureSize(int size) { + if (offset + size <= capacity) { + return; + } + + _expand(size); } - /// Checks if the [value] is within the specified [min] and [max] range. - /// - /// Throws a [RangeError] if the value is out of bounds. @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _checkRange(int value, int min, int max, String typeName) { - if (value < min || value > max) { - throw RangeError.range(value, min, max, typeName); + void ensureOneByte() { + if (offset + 1 <= capacity) { + return; } + + _expand(1); } - /// Ensures that the buffer has enough space to accommodate the specified - /// [size] bytes. - /// - /// If the buffer is too small, it expands using a 1.5x growth strategy, - /// which balances memory usage and reallocation frequency. - /// Uses O(1) calculation instead of loop for better performance. @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void _ensureSize(int size) { - final req = _offset + size; - if (req <= _capacity) { + void ensureTwoBytes() { + if (offset + 2 <= capacity) { + return; + } + + _expand(2); + } + + @pragma('vm:prefer-inline') + void ensureFourBytes() { + if (offset + 4 <= capacity) { + return; + } + + _expand(4); + } + + @pragma('vm:prefer-inline') + void ensureEightBytes() { + if (offset + 8 <= capacity) { return; } - var newCapacity = _capacity * 3 ~/ 2; // 1.5x + _expand(8); + } + + void _expand(int size) { + final req = offset + size; + var newCapacity = capacity * 2; if (newCapacity < req) { newCapacity = req; } - final newBuffer = Uint8List(newCapacity)..setRange(0, _offset, _buffer); - _buffer = newBuffer; - _capacity = newCapacity; + list = Uint8List(newCapacity)..setRange(0, offset, list); + + data = list.buffer.asByteData(); + capacity = newCapacity; } } diff --git a/lib/src/binary_writer_interface.dart b/lib/src/binary_writer_interface.dart deleted file mode 100644 index e93f8dc..0000000 --- a/lib/src/binary_writer_interface.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'dart:typed_data'; - -/// The [BinaryWriterInterface] class is an abstract base class used to encode -/// various types of data into a binary format. -abstract interface class BinaryWriterInterface { - /// Returns the number of bytes written to the buffer. - int get bytesWritten; - - /// Writes an 8-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 8-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position, and the offset is - /// incremented by 1 byte. - /// - /// The [value] parameter must be an unsigned 8-bit integer - /// (range: 0 to 255). - /// - /// Example: - /// ```dart - /// writer.writeUint8(200); // Writes the value 200 as a single byte. - /// ``` - void writeUint8(int value); - - /// Writes an 8-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 8-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position, and the offset is - /// incremented by 1 byte. - /// - /// The [value] parameter must be a signed 8-bit integer - /// (range: -128 to 127). - /// - /// Example: - /// ```dart - /// writer.writeInt8(-5); // Writes the value -5 as a single byte. - /// ``` - void writeInt8(int value); - - /// Writes a 16-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 16-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 2 bytes. - /// - /// The [value] parameter must be an unsigned 16-bit integer - /// (range: 0 to 65535). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeUint16(500); // Writes the value 500 as two bytes in big-endian order. - /// writer.writeUint16(500, .little); // Writes the value 500 as two bytes in little-endian order. - /// ``` - void writeUint16(int value, [Endian endian = .big]); - - /// Writes a 16-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 16-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 2 bytes. - /// - /// The [value] parameter must be a signed 16-bit integer - /// (range: -32768 to 32767). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeInt16(-100); // Writes the value -100 as two bytes in big-endian order. - /// writer.writeInt16(-100, .little); // Writes the value -100 as two bytes in little-endian order. - /// ``` - void writeInt16(int value, [Endian endian = .big]); - - /// Writes a 32-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 32-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 4 bytes. - /// - /// The [value] parameter must be an unsigned 32-bit integer - /// (range: 0 to 4294967295). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeUint32(100000); // Writes the value 100000 as four bytes in big-endian order. - /// writer.writeUint32(100000, .little); // Writes the value 100000 as four bytes in little-endian order. - /// ``` - void writeUint32(int value, [Endian endian = .big]); - - /// Writes a 32-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 32-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the specified - /// byte order (endian), and the offset is incremented by 4 bytes. - /// - /// The [value] parameter must be a signed 32-bit integer - /// (range: -2147483648 to 2147483647). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// writer.writeInt32(-50000); // Writes the value -50000 as four bytes in big-endian order. - /// writer.writeInt32(-50000, .little); // Writes the value -50000 as four bytes in little-endian order. - /// ``` - void writeInt32(int value, [Endian endian = .big]); - - /// Writes a 64-bit unsigned integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 64-bit unsigned integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the - /// specified byte order (endian), and the offset is incremented by 8 - /// bytes. - /// - /// The [value] parameter must be an unsigned 64-bit integer - /// (range: 0 to 18446744073709551615). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeUint64(10000000000); // Writes the value 10000000000 as eight bytes in big-endian order. - /// writer.writeUint64(10000000000, .little); // Writes the value 10000000000 as eight bytes in little-endian order. - /// ``` - void writeUint64(int value, [Endian endian = .big]); - - /// Writes a 64-bit signed integer to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 64-bit signed integer. If necessary, it expands the buffer size. The - /// integer is then written at the current offset position with the - /// specified byte order (endian), and the offset is incremented by 8 - /// bytes. - /// - /// The [value] parameter must be a signed 64-bit integer - /// (range: -9223372036854775808 to 9223372036854775807). - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeInt64(-10000000000); // Writes the value -10000000000 as eight bytes in big-endian order. - /// writer.writeInt64(-10000000000, .little); // Writes the value -10000000000 as eight bytes in little-endian order. - /// ``` - void writeInt64(int value, [Endian endian = .big]); - - /// Writes a 32-bit floating point number to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 32-bit float. If necessary, it expands the buffer size. The float is then - /// written at the current offset position with the specified byte - /// order (endian), and the offset is incremented by 4 bytes. - /// - /// The [value] parameter must be a 32-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use - /// (defaults to [.big]). - /// - /// Throws [RangeError] if [value] is out of range. - /// - /// Example: - /// ```dart - /// writer.writeFloat32(3.14); // Writes the value 3.14 as four bytes in big-endian order. - /// writer.writeFloat32(3.14, .little); // Writes the value 3.14 as four bytes in little-endian order. - /// ``` - void writeFloat32(double value, [Endian endian = .big]); - - /// Writes a 64-bit floating point number to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// 64-bit float. If necessary, it expands the buffer size. The float is then - /// written at the current offset position with the specified byte - /// order (endian), and the offset is incremented by 8 bytes. - /// - /// The [value] parameter must be a 64-bit floating point number. - /// The optional [endian] parameter specifies the byte order to use (defaults - /// to [.big]). - /// - /// Example: - /// ```dart - /// writer.writeFloat64(3.14); // Writes the value 3.14 as eight bytes in big-endian order. - /// writer.writeFloat64(3.14, .little); // Writes the value 3.14 as eight bytes in little-endian order. - /// ``` - void writeFloat64(double value, [Endian endian = .big]); - - /// Writes a list of bytes to the buffer. - /// - /// This method ensures that there is enough space in the buffer to write the - /// provided list of bytes. If necessary, it expands the buffer size. The - /// bytes are then written at the current offset position. If the offset is 0, - /// the bytes are added directly to the builder. Otherwise, the bytes are - /// copied to the buffer, either directly (if the list is a [Uint8List]) or - /// byte by byte. - /// - /// The [bytes] parameter must be a list of integers, where each integer is - /// between 0 and 255 inclusive. The list may be retained until [takeBytes] - /// is called. - /// - /// Example: - /// ```dart - /// writer.writeBytes([1, 2, 3, 4, 5]); // Writes the bytes 1, 2, 3, 4, and 5 to the buffer. - /// ``` - void writeBytes(Iterable bytes); - - /// Writes a UTF-8 encoded string to the buffer. - /// - /// This method encodes the provided string using UTF-8 encoding and writes - /// the resulting bytes to the buffer. If necessary, it expands the buffer - /// size to accommodate the encoded string. The encoded bytes are then written - /// at the current offset position, and the offset is incremented by the - /// length of the encoded string. - /// - /// The [value] parameter is the string to be encoded and written to the - /// buffer. - /// - /// The optional [allowMalformed] parameter specifies whether to allow - /// malformed UTF-16 sequences (lone surrogates). If false, a - /// [FormatException] - /// is thrown when encountering invalid surrogate pairs. If true (default), - /// invalid surrogates are replaced with the Unicode replacement character - /// U+FFFD (�). - /// - /// Example: - /// ```dart - /// writer.writeString("Hello, world!"); // Writes the string "Hello, world!" as UTF-8 bytes to the buffer. - /// writer.writeString("Test\uD800End", allowMalformed: false); // Throws FormatException for lone surrogate - /// ``` - void writeString(String value, {bool allowMalformed = true}); - - /// Returns the written bytes as a [Uint8List] and resets the writer. - /// - /// This method returns a copy of the written bytes from the beginning to the - /// current offset position. After returning the bytes, it resets the internal - /// state by clearing the offset and reinitializing the buffer to its initial - /// size, preparing the writer for new data. - /// - /// Use this method when you want to retrieve the data and start fresh. - /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint8(42); - /// final bytes = writer.takeBytes(); // Returns [42] and resets the writer - /// writer.writeUint8(100); // Can write new data - /// ``` - Uint8List takeBytes(); - - /// Returns the written bytes as a [Uint8List] without resetting the writer. - /// - /// This method returns a view of the written bytes from the beginning to the - /// current offset position. Unlike [takeBytes], this method does not reset - /// the internal state, allowing you to continue writing more data. - /// - /// Use this method when you want to inspect the current buffer state without - /// losing the ability to continue writing. - /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint8(42); - /// final bytes = writer.toBytes(); // Returns [42] without resetting - /// writer.writeUint8(100); // Continues writing, buffer is now [42, 100] - /// ``` - Uint8List toBytes(); - - /// Resets the writer to its initial state. - /// - /// This method resets the offset to 0 and reinitializes the buffer to its - /// initial size. Unlike [takeBytes], this method does not return the written - /// bytes, making it useful when you want to discard the current data and - /// start fresh. - /// - /// Use this method when you want to clear the buffer without retrieving data. - /// - /// Example: - /// ```dart - /// final writer = BinaryWriter(); - /// writer.writeUint8(42); - /// writer.reset(); // Resets the writer without returning bytes - /// writer.writeUint8(100); // Starts fresh with new data - /// ``` - void reset(); -} diff --git a/lib/src/fast_binary_reader.dart b/lib/src/fast_binary_reader.dart deleted file mode 100644 index d997d28..0000000 --- a/lib/src/fast_binary_reader.dart +++ /dev/null @@ -1,225 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -extension type const FastBinaryReader._(_Reader _ctx) { - FastBinaryReader(Uint8List buffer) : this._(_Reader(buffer)); - - @pragma('vm:prefer-inline') - int get availableBytes => _ctx.length - _ctx.offset; - - @pragma('vm:prefer-inline') - int get offset => _ctx.offset; - - @pragma('vm:prefer-inline') - int get length => _ctx.length; - - @pragma('vm:prefer-inline') - void _checkBounds(int bytes, String type, [int? offset]) { - assert( - (offset ?? _ctx.offset) + bytes <= _ctx.length, - 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_ctx.length - _ctx.offset} bytes at offset ${_ctx.offset}', - ); - } - - @pragma('vm:prefer-inline') - int readVarInt() { - var result = 0; - var shift = 0; - - final list = _ctx.list; - var offset = _ctx.offset; - - for (var i = 0; i < 10; i++) { - assert(offset < _ctx.length, 'VarInt out of bounds'); - final byte = list[offset++]; - - result |= (byte & 0x7f) << shift; - - if ((byte & 0x80) == 0) { - _ctx.offset = offset; - return result; - } - - shift += 7; - } - - throw const FormatException('VarInt is too long (more than 10 bytes)'); - } - - @pragma('vm:prefer-inline') - int readZigZag() { - final v = readVarInt(); - // Decode zig-zag encoding - return (v >>> 1) ^ -(v & 1); - } - - @pragma('vm:prefer-inline') - int readUint8() { - _checkBounds(1, 'Uint8'); - - return _ctx.data.getUint8(_ctx.offset++); - } - - @pragma('vm:prefer-inline') - int readInt8() { - _checkBounds(1, 'Int8'); - - return _ctx.data.getInt8(_ctx.offset++); - } - - @pragma('vm:prefer-inline') - int readUint16([Endian endian = .big]) { - _checkBounds(2, 'Uint16'); - - final value = _ctx.data.getUint16(_ctx.offset, endian); - _ctx.offset += 2; - - return value; - } - - @pragma('vm:prefer-inline') - int readInt16([Endian endian = .big]) { - _checkBounds(2, 'Int16'); - - final value = _ctx.data.getInt16(_ctx.offset, endian); - _ctx.offset += 2; - - return value; - } - - @pragma('vm:prefer-inline') - int readUint32([Endian endian = .big]) { - _checkBounds(4, 'Uint32'); - - final value = _ctx.data.getUint32(_ctx.offset, endian); - _ctx.offset += 4; - return value; - } - - @pragma('vm:prefer-inline') - int readInt32([Endian endian = .big]) { - _checkBounds(4, 'Int32'); - final value = _ctx.data.getInt32(_ctx.offset, endian); - _ctx.offset += 4; - return value; - } - - @pragma('vm:prefer-inline') - int readUint64([Endian endian = .big]) { - _checkBounds(8, 'Uint64'); - final value = _ctx.data.getUint64(_ctx.offset, endian); - _ctx.offset += 8; - return value; - } - - @pragma('vm:prefer-inline') - int readInt64([Endian endian = .big]) { - _checkBounds(8, 'Int64'); - final value = _ctx.data.getInt64(_ctx.offset, endian); - _ctx.offset += 8; - return value; - } - - @pragma('vm:prefer-inline') - double readFloat32([Endian endian = .big]) { - _checkBounds(4, 'Float32'); - - final value = _ctx.data.getFloat32(_ctx.offset, endian); - _ctx.offset += 4; - - return value; - } - - @pragma('vm:prefer-inline') - double readFloat64([Endian endian = .big]) { - _checkBounds(8, 'Float64'); - - final value = _ctx.data.getFloat64(_ctx.offset, endian); - _ctx.offset += 8; - return value; - } - - @pragma('vm:prefer-inline') - Uint8List readBytes(int length) { - assert(length >= 0, 'Length must be non-negative'); - _checkBounds(length, 'Bytes'); - - // Create a view of the underlying buffer without copying. - final bOffset = _ctx.baseOffset; - final bytes = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); - - _ctx.offset += length; - - return bytes; - } - - @pragma('vm:prefer-inline') - String readString(int length, {bool allowMalformed = false}) { - if (length == 0) { - return ''; - } - - _checkBounds(length, 'String'); - - final bOffset = _ctx.baseOffset; - final view = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); - _ctx.offset += length; - - return utf8.decode(view, allowMalformed: allowMalformed); - } - - @pragma('vm:prefer-inline') - Uint8List peekBytes(int length, [int? offset]) { - assert(length >= 0, 'Length must be non-negative'); - - if (length == 0) { - return Uint8List(0); - } - - final peekOffset = offset ?? _ctx.offset; - _checkBounds(length, 'Peek Bytes', peekOffset); - - final bOffset = _ctx.baseOffset; - - return _ctx.data.buffer.asUint8List(bOffset + peekOffset, length); - } - - void skip(int length) { - assert(length >= 0, 'Length must be non-negative'); - _checkBounds(length, 'Skip'); - - _ctx.offset += length; - } - - @pragma('vm:prefer-inline') - void reset() { - _ctx.offset = 0; - } -} - -final class _Reader { - _Reader(Uint8List buffer) - : list = buffer, - data = ByteData.sublistView(buffer).asUnmodifiableView(), - buffer = buffer.buffer, - length = buffer.length, - baseOffset = buffer.offsetInBytes, - offset = 0; - - final Uint8List list; - - /// Efficient view for typed data access. - final ByteData data; - - final ByteBuffer buffer; - - /// Total length of the buffer. - final int length; - - /// Current read position in the buffer. - late int offset; - - - final int baseOffset; -} diff --git a/lib/src/fast_binary_writer.dart b/lib/src/fast_binary_writer.dart deleted file mode 100644 index eab1198..0000000 --- a/lib/src/fast_binary_writer.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'dart:typed_data'; - -extension type FastBinaryWriter._(_Writer _ctx) { - FastBinaryWriter({int initialBufferSize = 128}) - : this._(_Writer(initialBufferSize)); - - int get bytesWritten => _ctx.offset; - - @pragma('vm:prefer-inline') - void _checkRange(int value, int min, int max, String typeName) { - if (value < min || value > max) { - throw RangeError.range(value, min, max, typeName); - } - } - - @pragma('vm:prefer-inline') - void writeVarInt(int value) { - // Fast path for single-byte VarInt - if (value < 0x80 && value >= 0) { - _ctx.ensureOneByte(); - _ctx.list[_ctx.offset++] = value; - return; - } - - _ctx.ensureSize(10); - - var v = value; - final list = _ctx.list; - var offset = _ctx.offset; - - while (v >= 0x80) { - list[offset++] = (v & 0x7F) | 0x80; - v >>>= 7; - } - - list[offset++] = v & 0x7F; - _ctx.offset = offset; - } - - void writeZigZag(int value) { - // Encode zig-zag encoding - final encoded = (value << 1) ^ (value >> 63); - writeVarInt(encoded); - } - - @pragma('vm:prefer-inline') - void writeUint8(int value) { - _checkRange(value, 0, 255, 'Uint8'); - _ctx.ensureOneByte(); - - _ctx.list[_ctx.offset++] = value; - } - - @pragma('vm:prefer-inline') - void writeInt8(int value) { - _checkRange(value, -128, 127, 'Int8'); - _ctx.ensureOneByte(); - - _ctx.list[_ctx.offset++] = value & 0xFF; - } - - @pragma('vm:prefer-inline') - void writeUint16(int value, [Endian endian = .big]) { - _checkRange(value, 0, 65535, 'Uint16'); - _ctx.ensureTwoBytes(); - - _ctx.data.setUint16(_ctx.offset, value, endian); - _ctx.offset += 2; - } - - @pragma('vm:prefer-inline') - void writeInt16(int value, [Endian endian = .big]) { - _checkRange(value, -32768, 32767, 'Int16'); - _ctx.ensureTwoBytes(); - - _ctx.data.setInt16(_ctx.offset, value, endian); - _ctx.offset += 2; - } - - @pragma('vm:prefer-inline') - void writeUint32(int value, [Endian endian = .big]) { - _checkRange(value, 0, 4294967295, 'Uint32'); - _ctx.ensureFourBytes(); - - _ctx.data.setUint32(_ctx.offset, value, endian); - _ctx.offset += 4; - } - - @pragma('vm:prefer-inline') - void writeInt32(int value, [Endian endian = .big]) { - _checkRange(value, -2147483648, 2147483647, 'Int32'); - _ctx.ensureFourBytes(); - - _ctx.data.setInt32(_ctx.offset, value, endian); - _ctx.offset += 4; - } - - @pragma('vm:prefer-inline') - void writeUint64(int value, [Endian endian = .big]) { - _checkRange(value, 0, 9223372036854775807, 'Uint64'); - _ctx.ensureEightBytes(); - - _ctx.data.setUint64(_ctx.offset, value, endian); - _ctx.offset += 8; - } - - @pragma('vm:prefer-inline') - void writeInt64(int value, [Endian endian = .big]) { - _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); - _ctx.ensureEightBytes(); - - _ctx.data.setInt64(_ctx.offset, value, endian); - _ctx.offset += 8; - } - - @pragma('vm:prefer-inline') - void writeFloat32(double value, [Endian endian = .big]) { - _ctx.ensureFourBytes(); - _ctx.data.setFloat32(_ctx.offset, value, endian); - _ctx.offset += 4; - } - - @pragma('vm:prefer-inline') - void writeFloat64(double value, [Endian endian = .big]) { - _ctx.ensureEightBytes(); - _ctx.data.setFloat64(_ctx.offset, value, endian); - _ctx.offset += 8; - } - - @pragma('vm:prefer-inline') - void writeBytes(List bytes, [int offset = 0, int? length]) { - final len = length ?? (bytes.length - offset); - _ctx.ensureSize(len); - - _ctx.list.setRange(_ctx.offset, _ctx.offset + len, bytes, offset); - _ctx.offset += len; - } - - @pragma('vm:prefer-inline') - void writeString(String value, {bool allowMalformed = true}) { - final len = value.length; - if (len == 0) { - return; - } - - // Pre-allocate: worst case for UTF-16 to UTF-8 is 3 bytes per code unit. - // (Surrogate pairs are 2 units -> 4 bytes, which is 2 bytes/unit). - _ctx.ensureSize(len * 3); - - final list = _ctx.list; - var offset = _ctx.offset; - var i = 0; - - while (i < len) { - var c = value.codeUnitAt(i); - - if (c < 0x80) { - // ------------------------------------------------------- - // ASCII Fast Path - // ------------------------------------------------------- - list[offset++] = c; - i++; - - // Unrolled loop for blocks of 4 ASCII characters - while (i <= len - 4) { - final c0 = value.codeUnitAt(i); - final c1 = value.codeUnitAt(i + 1); - final c2 = value.codeUnitAt(i + 2); - final c3 = value.codeUnitAt(i + 3); - - if ((c0 | c1 | c2 | c3) < 0x80) { - list[offset] = c0; - list[offset + 1] = c1; - list[offset + 2] = c2; - list[offset + 3] = c3; - offset += 4; - i += 4; - } else { - break; - } - } - - // Catch remaining ASCII characters before multi-byte logic - while (i < len) { - c = value.codeUnitAt(i); - if (c >= 0x80) { - break; - } - list[offset++] = c; - i++; - } - - if (i == len) { - break; - } - } - - // ------------------------------------------------------- - // Multi-byte handling - // ------------------------------------------------------- - if (c < 0x800) { - // 2 bytes: Cyrillic, Greek, Arabic, etc. - list[offset++] = 0xC0 | (c >> 6); - list[offset++] = 0x80 | (c & 0x3F); - i++; - } else if (c < 0xD800 || c > 0xDFFF) { - // 3 bytes: Basic Multilingual Plane - list[offset++] = 0xE0 | (c >> 12); - list[offset++] = 0x80 | ((c >> 6) & 0x3F); - list[offset++] = 0x80 | (c & 0x3F); - i++; - } else if (c <= 0xDBFF && i + 1 < len) { - // 4 bytes: Valid Surrogate Pair - final next = value.codeUnitAt(i + 1); - if (next >= 0xDC00 && next <= 0xDFFF) { - final codePoint = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); - list[offset++] = 0xF0 | (codePoint >> 18); - list[offset++] = 0x80 | ((codePoint >> 12) & 0x3F); - list[offset++] = 0x80 | ((codePoint >> 6) & 0x3F); - list[offset++] = 0x80 | (codePoint & 0x3F); - i += 2; - } else { - offset = _handleMalformed(value, i, offset, allowMalformed); - i++; - } - } else { - // Malformed: Lone surrogate or end of string - offset = _handleMalformed(value, i, offset, allowMalformed); - i++; - } - } - - _ctx.offset = offset; - } - - @pragma('vm:prefer-inline') - int _handleMalformed(String v, int i, int offset, bool allow) { - if (!allow) { - throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); - } - final list = _ctx.list; - list[offset] = 0xEF; - list[offset + 1] = 0xBF; - list[offset + 2] = 0xBD; - return offset + 3; - } - - @pragma('vm:prefer-inline') - Uint8List takeBytes() { - final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); - _ctx._initializeBuffer(); - return result; - } - - @pragma('vm:prefer-inline') - Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); - - @pragma('vm:prefer-inline') - void reset() => _ctx._initializeBuffer(); -} - -final class _Writer { - _Writer(int initialBufferSize) - : _size = initialBufferSize, - capacity = initialBufferSize, - offset = 0, - list = Uint8List(initialBufferSize) { - data = list.buffer.asByteData(); - } - - /// Current write position in the buffer. - late int offset; - - /// Cached buffer capacity to avoid repeated length checks. - late int capacity; - - /// Underlying byte buffer. - late Uint8List list; - - /// ByteData view of the underlying buffer for efficient writes. - late ByteData data; - - /// Initial buffer size. - final int _size; - - @pragma('vm:prefer-inline') - void _initializeBuffer() { - list = Uint8List(_size); - data = list.buffer.asByteData(); - capacity = _size; - offset = 0; - } - - @pragma('vm:prefer-inline') - @pragma('dart2js:tryInline') - void ensureSize(int size) { - if (offset + size <= capacity) { - return; - } - - _expand(size); - } - - @pragma('vm:prefer-inline') - void ensureOneByte() { - if (offset + 1 <= capacity) { - return; - } - - _expand(1); - } - - @pragma('vm:prefer-inline') - void ensureTwoBytes() { - if (offset + 2 <= capacity) { - return; - } - - _expand(2); - } - - @pragma('vm:prefer-inline') - void ensureFourBytes() { - if (offset + 4 <= capacity) { - return; - } - - _expand(4); - } - - @pragma('vm:prefer-inline') - void ensureEightBytes() { - if (offset + 8 <= capacity) { - return; - } - - _expand(8); - } - - void _expand(int size) { - final req = offset + size; - var newCapacity = capacity * 2; - if (newCapacity < req) { - newCapacity = req; - } - - list = Uint8List(newCapacity)..setRange(0, offset, list); - - data = list.buffer.asByteData(); - capacity = newCapacity; - } -} diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart index c9d8209..23d2097 100644 --- a/test/binary_reader_performance_test.dart +++ b/test/binary_reader_performance_test.dart @@ -13,7 +13,7 @@ class BinaryReaderBenchmark extends BenchmarkBase { 'Some more data to increase buffer usage. ' 'The quick brown fox jumps over the lazy dog.'; - final writer = FastBinaryWriter() + final writer = BinaryWriter() ..writeUint8(42) ..writeInt8(-42) ..writeUint16(65535, .little) @@ -70,133 +70,6 @@ class BinaryReaderBenchmark extends BenchmarkBase { } } -class FastBinaryReaderBenchmark extends BenchmarkBase { - FastBinaryReaderBenchmark() : super('FastBinaryReader performance test'); - - late final FastBinaryReader reader; - - @override - void setup() { - const string = 'Hello, World!'; - const longString = - 'Some more data to increase buffer usage. ' - 'The quick brown fox jumps over the lazy dog.'; - - final writer = FastBinaryWriter() - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535, .little) - ..writeInt16(-32768, .little) - ..writeUint32(4294967295, .little) - ..writeInt32(-2147483648, .little) - ..writeUint64(9223372036854775807, .little) - ..writeInt64(-9223372036854775808, .little) - ..writeFloat32(3.14, .little) - ..writeFloat64(3.141592653589793, .little) - ..writeFloat64(2.718281828459045) - ..writeInt8(string.length) - ..writeString(string) - ..writeInt32(longString.length) - ..writeString(longString) - ..writeBytes([]) - ..writeBytes(List.filled(120, 100)); - - final buffer = writer.takeBytes(); - reader = FastBinaryReader(buffer); - } - - @override - void exercise() => run(); - - @override - void run() { - for (var i = 0; i < 1000; i++) { - final _ = reader.readUint8(); - final _ = reader.readInt8(); - final _ = reader.readUint16(.little); - final _ = reader.readInt16(.little); - final _ = reader.readUint32(.little); - final _ = reader.readInt32(.little); - final _ = reader.readUint64(.little); - final _ = reader.readInt64(.little); - final _ = reader.readFloat32(.little); - final _ = reader.readFloat64(.little); - final _ = reader.readFloat64(.little); - final length = reader.readInt8(); - final _ = reader.readString(length); - final longLength = reader.readInt32(); - final _ = reader.readString(longLength); - final _ = reader.readBytes(0); - final _ = reader.readBytes(120); - - assert(reader.availableBytes == 0, 'Not all bytes were read'); - reader.reset(); - } - } - - static void main() { - FastBinaryReaderBenchmark().report(); - } -} - -class VarIntReaderBenchmark extends BenchmarkBase { - VarIntReaderBenchmark() : super('VarIntReader performance test'); - - late final FastBinaryReader reader; - - @override - void setup() { - const string = 'Hello, World!'; - const longString = - 'Some more data to increase buffer usage. ' - 'The quick brown fox jumps over the lazy dog.'; - - final writer = FastBinaryWriter() - ..writeVarInt(1) - ..writeVarInt(300) - ..writeVarInt(70000) - ..writeVarInt(1 << 20) - ..writeVarInt(1 << 30) - ..writeInt8(string.length) - ..writeInt32(longString.length); - - final buffer = writer.takeBytes(); - reader = FastBinaryReader(buffer); - } - - @override - void exercise() => run(); - - @override - void run() { - for (var i = 0; i < 1000; i++) { - final v1 = reader.readVarInt(); - final v2 = reader.readVarInt(); - final v3 = reader.readVarInt(); - final v4 = reader.readVarInt(); - final v5 = reader.readVarInt(); - final length = reader.readInt8(); - final longLength = reader.readInt32(); - assert(v1 == 1, 'Unexpected VarInt value: $v1'); - assert(v2 == 300, 'Unexpected VarInt value: $v2'); - assert(v3 == 70000, 'Unexpected VarInt value: $v3'); - assert(v4 == 1 << 20, 'Unexpected VarInt value: $v4'); - assert(v5 == 1 << 30, 'Unexpected VarInt value: $v5'); - assert(length == 13, 'Unexpected string length: $length'); - assert(longLength == 85, 'Unexpected long string length: $longLength'); - - assert(reader.availableBytes == 0, 'Not all bytes were read'); - reader.reset(); - } - } - - static void main() { - VarIntReaderBenchmark().report(); - } -} - void main() { BinaryReaderBenchmark.main(); - FastBinaryReaderBenchmark.main(); - VarIntReaderBenchmark.main(); } diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 290822c..d25f67d 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -8,7 +8,7 @@ void main() { group('FastBinaryReader', () { test('readUint8', () { final buffer = Uint8List.fromList([0x01]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8(), equals(1)); expect(reader.availableBytes, equals(0)); @@ -16,7 +16,7 @@ void main() { test('readInt8', () { final buffer = Uint8List.fromList([0xFF]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt8(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -24,7 +24,7 @@ void main() { test('readUint16 big-endian', () { final buffer = Uint8List.fromList([0x01, 0x00]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint16(), equals(256)); expect(reader.availableBytes, equals(0)); @@ -32,7 +32,7 @@ void main() { test('readUint16 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint16(.little), equals(256)); expect(reader.availableBytes, equals(0)); @@ -40,7 +40,7 @@ void main() { test('readInt16 big-endian', () { final buffer = Uint8List.fromList([0xFF, 0xFF]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt16(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -48,7 +48,7 @@ void main() { test('readInt16 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x80]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt16(.little), equals(-32768)); expect(reader.availableBytes, equals(0)); @@ -56,7 +56,7 @@ void main() { test('readUint32 big-endian', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x00, 0x00]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint32(), equals(65536)); expect(reader.availableBytes, equals(0)); @@ -64,7 +64,7 @@ void main() { test('readUint32 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x00, 0x01, 0x00]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint32(.little), equals(65536)); expect(reader.availableBytes, equals(0)); @@ -72,7 +72,7 @@ void main() { test('readInt32 big-endian', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt32(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -80,7 +80,7 @@ void main() { test('readInt32 little-endian', () { final buffer = Uint8List.fromList([0x00, 0x00, 0x00, 0x80]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt32(.little), equals(-2147483648)); expect(reader.availableBytes, equals(0)); @@ -97,7 +97,7 @@ void main() { 0x00, 0x00, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint64(), equals(4294967296)); expect(reader.availableBytes, equals(0)); @@ -114,7 +114,7 @@ void main() { 0x00, 0x00, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint64(.little), equals(4294967296)); expect(reader.availableBytes, equals(0)); @@ -131,7 +131,7 @@ void main() { 0xFF, 0xFF, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt64(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -148,7 +148,7 @@ void main() { 0x00, 0x80, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt64(.little), equals(-9223372036854775808)); expect(reader.availableBytes, equals(0)); @@ -156,7 +156,7 @@ void main() { test('readFloat32 big-endian', () { final buffer = Uint8List.fromList([0x40, 0x49, 0x0F, 0xDB]); // 3.1415927 - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32(), closeTo(3.1415927, 0.0000001)); expect(reader.availableBytes, equals(0)); @@ -164,7 +164,7 @@ void main() { test('readFloat32 little-endian', () { final buffer = Uint8List.fromList([0xDB, 0x0F, 0x49, 0x40]); // 3.1415927 - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32(.little), closeTo(3.1415927, 0.0000001)); expect(reader.availableBytes, equals(0)); @@ -181,7 +181,7 @@ void main() { 0x2D, 0x18, ]); // 3.141592653589793 - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect( reader.readFloat64(), @@ -201,7 +201,7 @@ void main() { 0x09, 0x40, ]); // 3.141592653589793 - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect( reader.readFloat64(.little), @@ -212,7 +212,7 @@ void main() { test('readVarInt single byte (0)', () { final buffer = Uint8List.fromList([0]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(0)); expect(reader.availableBytes, equals(0)); @@ -220,7 +220,7 @@ void main() { test('readVarInt single byte (127)', () { final buffer = Uint8List.fromList([127]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(127)); expect(reader.availableBytes, equals(0)); @@ -228,7 +228,7 @@ void main() { test('readVarInt two bytes (128)', () { final buffer = Uint8List.fromList([0x80, 0x01]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(128)); expect(reader.availableBytes, equals(0)); @@ -236,7 +236,7 @@ void main() { test('readVarInt two bytes (300)', () { final buffer = Uint8List.fromList([0xAC, 0x02]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(300)); expect(reader.availableBytes, equals(0)); @@ -244,7 +244,7 @@ void main() { test('readVarInt three bytes (16384)', () { final buffer = Uint8List.fromList([0x80, 0x80, 0x01]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(16384)); expect(reader.availableBytes, equals(0)); @@ -252,7 +252,7 @@ void main() { test('readVarInt four bytes (2097151)', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0x7F]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(2097151)); expect(reader.availableBytes, equals(0)); @@ -260,7 +260,7 @@ void main() { test('readVarInt five bytes (268435455)', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0x7F]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(268435455)); expect(reader.availableBytes, equals(0)); @@ -268,14 +268,14 @@ void main() { test('readVarInt large value', () { final buffer = Uint8List.fromList([0x80, 0x80, 0x80, 0x80, 0x04]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(1 << 30)); expect(reader.availableBytes, equals(0)); }); test('readVarInt roundtrip with writeVarInt', () { - final writer = FastBinaryWriter() + final writer = BinaryWriter() ..writeVarInt(0) ..writeVarInt(1) ..writeVarInt(127) @@ -286,7 +286,7 @@ void main() { ..writeVarInt(1 << 30); final buffer = writer.takeBytes(); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readVarInt(), equals(0)); expect(reader.readVarInt(), equals(1)); @@ -301,7 +301,7 @@ void main() { test('readZigZag encoding for zero', () { final buffer = Uint8List.fromList([0]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(0)); expect(reader.availableBytes, equals(0)); @@ -309,7 +309,7 @@ void main() { test('readZigZag encoding for positive value 1', () { final buffer = Uint8List.fromList([2]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(1)); expect(reader.availableBytes, equals(0)); @@ -317,7 +317,7 @@ void main() { test('readZigZag encoding for negative value -1', () { final buffer = Uint8List.fromList([1]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(-1)); expect(reader.availableBytes, equals(0)); @@ -325,7 +325,7 @@ void main() { test('readZigZag encoding for positive value 2', () { final buffer = Uint8List.fromList([4]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(2)); expect(reader.availableBytes, equals(0)); @@ -333,7 +333,7 @@ void main() { test('readZigZag encoding for negative value -2', () { final buffer = Uint8List.fromList([3]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(-2)); expect(reader.availableBytes, equals(0)); @@ -341,7 +341,7 @@ void main() { test('readZigZag encoding for large positive value', () { final buffer = Uint8List.fromList([0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(2147483647)); expect(reader.availableBytes, equals(0)); @@ -349,14 +349,14 @@ void main() { test('readZigZag encoding for large negative value', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(-2147483648)); expect(reader.availableBytes, equals(0)); }); test('readZigZag roundtrip with writeZigZag', () { - final writer = FastBinaryWriter() + final writer = BinaryWriter() ..writeZigZag(0) ..writeZigZag(1) ..writeZigZag(-1) @@ -368,7 +368,7 @@ void main() { ..writeZigZag(-2147483648); final buffer = writer.takeBytes(); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readZigZag(), equals(0)); expect(reader.readZigZag(), equals(1)); @@ -385,7 +385,7 @@ void main() { test('readBytes', () { final data = [0x01, 0x02, 0x03, 0x04, 0x05]; final buffer = Uint8List.fromList(data); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readBytes(5), equals(data)); expect(reader.availableBytes, equals(0)); @@ -395,7 +395,7 @@ void main() { const str = 'Hello, world!'; final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); expect(reader.availableBytes, equals(0)); @@ -405,7 +405,7 @@ void main() { const str = 'Привет, мир!'; // "Hello, world!" in Russian final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); expect(reader.availableBytes, equals(0)); @@ -413,7 +413,7 @@ void main() { test('availableBytes returns correct number of remaining bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.availableBytes, equals(4)); reader.readUint8(); @@ -424,7 +424,7 @@ void main() { test('usedBytes returns correct number of used bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.offset, equals(0)); reader.readUint8(); @@ -437,7 +437,7 @@ void main() { 'peekBytes returns correct bytes without changing the internal state', () { final buffer = Uint8List.fromList([0x10, 0x20, 0x30, 0x40, 0x50]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final peekedBytes = reader.peekBytes(3); expect(peekedBytes, equals([0x10, 0x20, 0x30])); @@ -451,14 +451,14 @@ void main() { test('skip method correctly updates the offset', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x02, 0x03, 0x04]); - final reader = FastBinaryReader(buffer)..skip(2); + final reader = BinaryReader(buffer)..skip(2); expect(reader.offset, equals(2)); expect(reader.readUint8(), equals(0x02)); }); test('read zero-length bytes', () { final buffer = Uint8List.fromList([]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readBytes(0), equals([])); expect(reader.availableBytes, equals(0)); @@ -466,14 +466,14 @@ void main() { test('read beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint32, throwsA(isA())); }); test('negative length input throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.readBytes(-1), throwsA(isA())); expect(() => reader.skip(-5), throwsA(isA())); @@ -482,21 +482,21 @@ void main() { test('reading from empty buffer', () { final buffer = Uint8List.fromList([]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8, throwsA(isA())); }); test('reading with offset at end of buffer', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = FastBinaryReader(buffer)..skip(2); + final reader = BinaryReader(buffer)..skip(2); expect(reader.readUint8, throwsA(isA())); }); test('peekBytes beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.peekBytes(3), throwsA(isA())); expect(() => reader.peekBytes(1, 2), throwsA(isA())); @@ -504,21 +504,21 @@ void main() { test('readString with insufficient bytes throws AssertionError', () { final buffer = Uint8List.fromList([0x48, 0x65]); // 'He' - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.readString(5), throwsA(isA())); }); test('readBytes with insufficient bytes throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.readBytes(3), throwsA(isA())); }); test('read methods throw AssertionError when not enough bytes', () { final buffer = Uint8List.fromList([0x00, 0x01]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint32, throwsA(isA())); expect(reader.readInt32, throwsA(isA())); @@ -529,7 +529,7 @@ void main() { 'readUint64 and readInt64 with insufficient bytes throw AssertionError', () { final buffer = Uint8List.fromList(List.filled(7, 0x00)); // Only 7 bytes - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint64, throwsA(isA())); expect(reader.readInt64, throwsA(isA())); @@ -538,7 +538,7 @@ void main() { test('skip beyond buffer throws AssertionError', () { final buffer = Uint8List.fromList([0x01, 0x02]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.skip(3), throwsA(isA())); }); @@ -553,7 +553,7 @@ void main() { 0xFF, 0xFF, 0xFF, 0xFF, // Int32 big-endian 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Float64 (double 2.0) ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8(), equals(0x01)); expect(reader.readInt8(), equals(-1)); @@ -568,7 +568,7 @@ void main() { const str = 'こんにちは世界'; // "Hello, World" in Japanese final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); }); @@ -576,42 +576,42 @@ void main() { group('Boundary checks', () { test('readUint8 throws when buffer is empty', () { final buffer = Uint8List.fromList([]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8, throwsA(isA())); }); test('readInt8 throws when buffer is empty', () { final buffer = Uint8List.fromList([]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt8, throwsA(isA())); }); test('readUint16 throws when only 1 byte available', () { final buffer = Uint8List.fromList([0x01]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint16, throwsA(isA())); }); test('readInt16 throws when only 1 byte available', () { final buffer = Uint8List.fromList([0xFF]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt16, throwsA(isA())); }); test('readUint32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint32, throwsA(isA())); }); test('readInt32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt32, throwsA(isA())); }); @@ -626,7 +626,7 @@ void main() { 0x06, 0x07, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint64, throwsA(isA())); }); @@ -641,14 +641,14 @@ void main() { 0xFF, 0xFF, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readInt64, throwsA(isA())); }); test('readFloat32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32, throwsA(isA())); }); @@ -663,35 +663,35 @@ void main() { 0x06, 0x07, ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat64, throwsA(isA())); }); test('readBytes throws when requested length exceeds available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.readBytes(5), throwsA(isA())); }); test('readBytes throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.readBytes(-1), throwsA(isA())); }); test('readString throws when requested length exceeds available', () { final buffer = Uint8List.fromList([0x48, 0x65, 0x6C]); // "Hel" - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.readString(10), throwsA(isA())); }); test('multiple reads exceed buffer size', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = FastBinaryReader(buffer) + final reader = BinaryReader(buffer) ..readUint8() // 1 byte read, 3 remaining ..readUint8() // 1 byte read, 2 remaining ..readUint16(); // 2 bytes read, 0 remaining @@ -701,21 +701,21 @@ void main() { test('peekBytes throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.peekBytes(-1), throwsA(isA())); }); test('skip throws when length exceeds available bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.skip(5), throwsA(isA())); }); test('skip throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(() => reader.skip(-1), throwsA(isA())); }); @@ -724,7 +724,7 @@ void main() { group('offset getter', () { test('offset returns current reading position', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.offset, equals(0)); @@ -740,7 +740,7 @@ void main() { test('offset resets to 0 after reset', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer)..readUint8(); + final reader = BinaryReader(buffer)..readUint8(); expect(reader.offset, equals(1)); expect(reader.availableBytes, equals(2)); @@ -753,7 +753,7 @@ void main() { group('Special values and edge cases', () { test('readString with empty UTF-8 string', () { final buffer = Uint8List.fromList([]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readString(0), equals('')); expect(reader.availableBytes, equals(0)); @@ -763,7 +763,7 @@ void main() { const str = '🚀👨‍👩‍👧‍👦'; // Rocket and family emoji final encoded = utf8.encode(str); final buffer = Uint8List.fromList(encoded); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readString(encoded.length), equals(str)); expect(reader.availableBytes, equals(0)); @@ -772,7 +772,7 @@ void main() { test('readFloat32 with NaN', () { final buffer = Uint8List(4); ByteData.view(buffer.buffer).setFloat32(0, double.nan); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32().isNaN, isTrue); }); @@ -780,7 +780,7 @@ void main() { test('readFloat32 with Infinity', () { final buffer = Uint8List(4); ByteData.view(buffer.buffer).setFloat32(0, double.infinity); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32(), equals(double.infinity)); }); @@ -788,7 +788,7 @@ void main() { test('readFloat32 with negative Infinity', () { final buffer = Uint8List(4); ByteData.view(buffer.buffer).setFloat32(0, double.negativeInfinity); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32(), equals(double.negativeInfinity)); }); @@ -796,7 +796,7 @@ void main() { test('readFloat64 with NaN', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, double.nan); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat64().isNaN, isTrue); }); @@ -804,7 +804,7 @@ void main() { test('readFloat64 with Infinity', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, double.infinity); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat64(), equals(double.infinity)); }); @@ -812,7 +812,7 @@ void main() { test('readFloat64 with negative Infinity', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, double.negativeInfinity); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat64(), equals(double.negativeInfinity)); }); @@ -820,7 +820,7 @@ void main() { test('readFloat64 with negative zero', () { final buffer = Uint8List(8); ByteData.view(buffer.buffer).setFloat64(0, -0); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final value = reader.readFloat64(); expect(value, equals(0.0)); @@ -831,7 +831,7 @@ void main() { final buffer = Uint8List.fromList([ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); // Max Uint64 is 2^64 - 1 = 18446744073709551615 // In Dart, this wraps to -1 for signed int representation @@ -840,7 +840,7 @@ void main() { test('peekBytes with zero length', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.peekBytes(0), equals([])); expect(reader.offset, equals(0)); @@ -848,7 +848,7 @@ void main() { test('peekBytes with explicit zero offset', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer)..readUint8(); + final reader = BinaryReader(buffer)..readUint8(); final peeked = reader.peekBytes(2, 0); expect(peeked, equals([0x01, 0x02])); @@ -857,7 +857,7 @@ void main() { test('multiple resets in sequence', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); - final reader = FastBinaryReader(buffer) + final reader = BinaryReader(buffer) ..readUint8() ..reset() ..reset() @@ -869,7 +869,7 @@ void main() { test('read after buffer exhaustion and reset', () { final buffer = Uint8List.fromList([0x42, 0x43]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8(), equals(0x42)); expect(reader.readUint8(), equals(0x43)); @@ -888,7 +888,7 @@ void main() { 0xFF, // Invalid byte 0x57, 0x6F, 0x72, 0x6C, 0x64, // "World" ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, contains('Hello')); @@ -897,7 +897,7 @@ void main() { test('readString with allowMalformed=false throws on invalid UTF-8', () { final buffer = Uint8List.fromList([0xFF, 0xFE, 0xFD]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect( () => reader.readString(buffer.length), @@ -907,7 +907,7 @@ void main() { test('readString handles truncated multi-byte sequence', () { final buffer = Uint8List.fromList([0xE0, 0xA0]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect( () => reader.readString(buffer.length), @@ -920,7 +920,7 @@ void main() { 0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello" 0xE0, 0xA0, // Incomplete 3-byte sequence ]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, startsWith('Hello')); @@ -930,7 +930,7 @@ void main() { group('Lone surrogate pairs', () { test('readString handles lone high surrogate', () { final buffer = utf8.encode('Test\uD800End'); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, isNotEmpty); @@ -938,7 +938,7 @@ void main() { test('readString handles lone low surrogate', () { final buffer = utf8.encode('Test\uDC00End'); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final result = reader.readString(buffer.length, allowMalformed: true); expect(result, isNotEmpty); @@ -950,7 +950,7 @@ void main() { 'peekBytes with offset beyond current position but within buffer', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - final reader = FastBinaryReader(buffer) + final reader = BinaryReader(buffer) ..readUint8() ..readUint8(); @@ -962,7 +962,7 @@ void main() { test('peekBytes at buffer boundary', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final peeked = reader.peekBytes(2, 3); expect(peeked, equals([4, 5])); @@ -971,7 +971,7 @@ void main() { test('peekBytes exactly at end with zero length', () { final buffer = Uint8List.fromList([1, 2, 3]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final peeked = reader.peekBytes(0, 3); expect(peeked, isEmpty); @@ -982,7 +982,7 @@ void main() { group('Sequential operations', () { test('multiple reset calls with intermediate reads', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8(), equals(1)); reader.reset(); @@ -995,7 +995,7 @@ void main() { test('alternating read and peek operations', () { final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint8(), equals(10)); expect(reader.peekBytes(2), equals([20, 30])); @@ -1013,7 +1013,7 @@ void main() { buffer[i] = i % 256; } - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final result = reader.readBytes(largeSize); expect(result.length, equals(largeSize)); @@ -1022,7 +1022,7 @@ void main() { test('skip large amount of data', () { final buffer = Uint8List(100000); - final reader = FastBinaryReader(buffer)..skip(50000); + final reader = BinaryReader(buffer)..skip(50000); expect(reader.offset, equals(50000)); expect(reader.availableBytes, equals(50000)); }); @@ -1031,8 +1031,8 @@ void main() { group('Buffer sharing', () { test('multiple readers can read same buffer concurrently', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader1 = FastBinaryReader(buffer); - final reader2 = FastBinaryReader(buffer); + final reader1 = BinaryReader(buffer); + final reader2 = BinaryReader(buffer); expect(reader1.readUint8(), equals(1)); expect(reader2.readUint8(), equals(1)); @@ -1042,7 +1042,7 @@ void main() { test('peekBytes returns independent views', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final peek1 = reader.peekBytes(3); final peek2 = reader.peekBytes(3); @@ -1056,7 +1056,7 @@ void main() { group('Zero-copy verification', () { test('readBytes returns view of original buffer', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final bytes = reader.readBytes(3); @@ -1066,7 +1066,7 @@ void main() { test('peekBytes returns view of original buffer', () { final buffer = Uint8List.fromList([10, 20, 30, 40, 50]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final peeked = reader.peekBytes(3); @@ -1084,7 +1084,7 @@ void main() { ..writeUint32(0x11223344, .little); final buffer = writer.takeBytes(); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint16(), equals(0x1234)); expect(reader.readUint16(.little), equals(0x5678)); @@ -1100,7 +1100,7 @@ void main() { ..writeFloat64(1.732, .little); final buffer = writer.takeBytes(); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readFloat32(), closeTo(3.14, 0.01)); expect(reader.readFloat32(.little), closeTo(2.71, 0.01)); @@ -1112,7 +1112,7 @@ void main() { group('Boundary conditions at exact sizes', () { test('buffer exactly matches read size', () { final buffer = Uint8List.fromList([1, 2, 3, 4]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); final result = reader.readBytes(4); expect(result, equals([1, 2, 3, 4])); @@ -1121,7 +1121,7 @@ void main() { test('reading exactly to boundary multiple times', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6]); - final reader = FastBinaryReader(buffer); + final reader = BinaryReader(buffer); expect(reader.readUint16(), equals(0x0102)); expect(reader.readUint16(), equals(0x0304)); @@ -1141,7 +1141,7 @@ void main() { // Create a view starting at offset 50 final subBuffer = Uint8List.sublistView(largeBuffer, 50, 60); - final reader = FastBinaryReader(subBuffer); + final reader = BinaryReader(subBuffer); // Read bytes and verify they match the expected values (50-59) final bytes = reader.readBytes(5); @@ -1164,7 +1164,7 @@ void main() { 30, 30 + encoded.length, ); - final reader = FastBinaryReader(subBuffer); + final reader = BinaryReader(subBuffer); final result = reader.readString(encoded.length); expect(result, equals(text)); @@ -1179,7 +1179,7 @@ void main() { // Create a view starting at offset 20 final subBuffer = Uint8List.sublistView(largeBuffer, 20, 30); - final reader = FastBinaryReader(subBuffer); + final reader = BinaryReader(subBuffer); // Peek at bytes without consuming them final peeked = reader.peekBytes(5); @@ -1196,7 +1196,7 @@ void main() { final largeBuffer = Uint8List(100); // Write some values at offset 40 - final writer = FastBinaryWriter() + final writer = BinaryWriter() ..writeUint16(0x1234) ..writeUint32(0x56789ABC) // disabling lint for large integer literal @@ -1212,7 +1212,7 @@ void main() { 40, 40 + data.length, ); - final reader = FastBinaryReader(subBuffer); + final reader = BinaryReader(subBuffer); expect(reader.readUint16(), equals(0x1234)); expect(reader.readUint32(), equals(0x56789ABC)); @@ -1229,10 +1229,10 @@ void main() { } // Create two readers from different offsets - final reader1 = FastBinaryReader( + final reader1 = BinaryReader( Uint8List.sublistView(largeBuffer, 10, 20), ); - final reader2 = FastBinaryReader( + final reader2 = BinaryReader( Uint8List.sublistView(largeBuffer, 50, 60), ); @@ -1255,7 +1255,7 @@ void main() { 75, 75 + encoded.length, ); - final reader = FastBinaryReader(subBuffer); + final reader = BinaryReader(subBuffer); final result = reader.readString(encoded.length); expect(result, equals(text)); diff --git a/test/binary_writer_performance_test.dart b/test/binary_writer_performance_test.dart index d6db688..411d4f4 100644 --- a/test/binary_writer_performance_test.dart +++ b/test/binary_writer_performance_test.dart @@ -103,65 +103,6 @@ class BinaryWriterBenchmark extends BenchmarkBase { } } -class FastBinaryWriterBenchmark extends BenchmarkBase { - FastBinaryWriterBenchmark() : super('FastBinaryWriter performance test'); - - late final FastBinaryWriter writer; - - @override - void setup() { - writer = FastBinaryWriter(); - } - - @override - void run() { - for (var i = 0; i < 1000; i++) { - writer - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535, .little) - ..writeUint16(10) - ..writeInt16(-32768, .little) - ..writeInt16(-10) - ..writeUint32(4294967295, .little) - ..writeUint32(100) - ..writeInt32(-2147483648, .little) - ..writeInt32(-100) - ..writeUint64(9223372036854775807, .little) - ..writeUint64(1000) - ..writeInt64(-9223372036854775808, .little) - ..writeInt64(-1000) - ..writeFloat32(3.14, .little) - ..writeFloat32(2.71) - ..writeFloat64(3.141592653589793, .little) - ..writeFloat64(2.718281828459045) - ..writeBytes(listUint8) - ..writeBytes(listUint16) - ..writeBytes(listUint32) - ..writeBytes(listFloat32) - ..writeString(shortString) - ..writeString(longStringWithEmoji); - - final bytes = writer.takeBytes(); - - if (writer.bytesWritten != 0) { - throw StateError('bytesWritten should be reset to 0 after takeBytes()'); - } - - if (bytes.length != 1432) { - throw StateError('Unexpected byte length: ${bytes.length}'); - } - } - } - - @override - void exercise() => run(); - static void main() { - FastBinaryWriterBenchmark().report(); - } -} - void main() { BinaryWriterBenchmark.main(); - FastBinaryWriterBenchmark.main(); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 11c026a..5c9e4fa 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -5,10 +5,10 @@ import 'package:test/test.dart'; void main() { group('BinaryWriter', () { - late FastBinaryWriter writer; + late BinaryWriter writer; setUp(() { - writer = FastBinaryWriter(); + writer = BinaryWriter(); }); test('should return empty list when takeBytes called on empty writer', () { diff --git a/test/integration_test.dart b/test/integration_test.dart index bfdbb38..aaa7fa8 100644 --- a/test/integration_test.dart +++ b/test/integration_test.dart @@ -560,10 +560,10 @@ void main() { final reader = BinaryReader(bytes); expect(reader.readUint32(), equals(100)); - expect(reader.usedBytes, equals(4)); + expect(reader.offset, equals(4)); reader.reset(); - expect(reader.usedBytes, equals(0)); + expect(reader.offset, equals(0)); expect(reader.readUint32(), equals(100)); }); From 8a08880d29aa56c058fa854caa2ed2375b16d261 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 25 Dec 2025 14:15:46 +0200 Subject: [PATCH 05/29] Enhance documentation for BinaryReader and BinaryWriter with detailed method descriptions and examples --- lib/src/binary_reader.dart | 174 ++++++++++++++++++++++++++++---- lib/src/binary_writer.dart | 199 +++++++++++++++++++++++++++++++------ 2 files changed, 324 insertions(+), 49 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 5f86fbd..7e6412b 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -1,27 +1,55 @@ import 'dart:convert'; import 'dart:typed_data'; -extension type const BinaryReader._(_Reader _ctx) { - BinaryReader(Uint8List buffer) : this._(_Reader(buffer)); - +/// A high-performance binary reader for decoding data from a byte buffer. +/// +/// Provides methods for reading various data types including: +/// - Variable-length integers (VarInt, ZigZag) +/// - Fixed-width integers (8, 16, 32, 64-bit signed and unsigned) +/// - Floating-point numbers (32 and 64-bit) +/// - Byte arrays and strings +/// +/// The reader maintains an internal offset that advances as data is read. +/// Use [reset] to restart reading from the beginning. +/// +/// Example: +/// ```dart +/// final reader = BinaryReader(bytes); +/// final value = reader.readUint32(); +/// final text = reader.readString(10); +/// ``` +extension type const BinaryReader._(_ReaderState _ctx) { + /// Creates a new [BinaryReader] from the given byte buffer. + /// + /// The reader will start at position 0 and can read up to `buffer.length` + /// bytes. + BinaryReader(Uint8List buffer) : this._(_ReaderState(buffer)); + + /// Returns the number of bytes remaining to be read. @pragma('vm:prefer-inline') int get availableBytes => _ctx.length - _ctx.offset; + /// Returns the current read position in the buffer. @pragma('vm:prefer-inline') int get offset => _ctx.offset; + /// Returns the total length of the buffer in bytes. @pragma('vm:prefer-inline') int get length => _ctx.length; - @pragma('vm:prefer-inline') - void _checkBounds(int bytes, String type, [int? offset]) { - assert( - (offset ?? _ctx.offset) + bytes <= _ctx.length, - 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_ctx.length - _ctx.offset} bytes at offset ${_ctx.offset}', - ); - } - + /// Reads a variable-length integer encoded using VarInt format. + /// + /// VarInt encoding uses the lower 7 bits of each byte for data and the + /// highest bit as a continuation flag. This format is space-efficient + /// for small numbers (1-5 bytes for typical 32-bit values). + /// + /// The algorithm: + /// 1. Read a byte and extract the lower 7 bits + /// 2. If the 8th bit is set, continue reading + /// 3. Shift and combine all 7-bit chunks + /// + /// Throws [FormatException] if the VarInt exceeds 10 bytes (malformed data). + /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') int readVarInt() { var result = 0; @@ -30,12 +58,15 @@ extension type const BinaryReader._(_Reader _ctx) { final list = _ctx.list; var offset = _ctx.offset; + // VarInt uses up to 10 bytes for 64-bit integers for (var i = 0; i < 10; i++) { assert(offset < _ctx.length, 'VarInt out of bounds'); final byte = list[offset++]; + // Extract lower 7 bits and shift into position result |= (byte & 0x7f) << shift; + // If MSB is 0, we've reached the last byte if ((byte & 0x80) == 0) { _ctx.offset = offset; return result; @@ -47,13 +78,24 @@ extension type const BinaryReader._(_Reader _ctx) { throw const FormatException('VarInt is too long (more than 10 bytes)'); } + /// Reads a ZigZag-encoded signed integer. + /// + /// ZigZag encoding maps signed integers to unsigned values such that + /// small absolute values (both positive and negative) use fewer bytes: + /// - 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. + /// + /// Decoding formula: (n >>> 1) ^ -(n & 1) + /// This reverses the encoding: (n << 1) ^ (n >> 63) @pragma('vm:prefer-inline') int readZigZag() { final v = readVarInt(); - // Decode zig-zag encoding + // Decode: right shift by 1, XOR with sign-extended LSB return (v >>> 1) ^ -(v & 1); } + /// Reads an 8-bit unsigned integer (0-255). + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint8() { _checkBounds(1, 'Uint8'); @@ -61,6 +103,9 @@ extension type const BinaryReader._(_Reader _ctx) { return _ctx.data.getUint8(_ctx.offset++); } + /// Reads an 8-bit signed integer (-128 to 127). + /// + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt8() { _checkBounds(1, 'Int8'); @@ -68,6 +113,10 @@ extension type const BinaryReader._(_Reader _ctx) { return _ctx.data.getInt8(_ctx.offset++); } + /// Reads a 16-bit unsigned integer (0-65535). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); @@ -78,6 +127,10 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 16-bit signed integer (-32768 to 32767). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); @@ -88,6 +141,10 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 32-bit unsigned integer (0 to 4,294,967,295). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); @@ -97,6 +154,10 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); @@ -105,6 +166,11 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 64-bit unsigned integer. + /// + /// Note: Dart's integer precision is limited to 2^53 on web targets. + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); @@ -113,6 +179,11 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 64-bit signed integer. + /// + /// Note: Dart's integer precision is limited to 2^53 on web targets. + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); @@ -121,6 +192,10 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 32-bit floating-point number (IEEE 754 single precision). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); @@ -131,6 +206,10 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a 64-bit floating-point number (IEEE 754 double precision). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); @@ -140,12 +219,19 @@ extension type const BinaryReader._(_Reader _ctx) { return value; } + /// Reads a sequence of bytes and returns them as a [Uint8List]. + /// + /// Returns a view of the underlying buffer without copying data, + /// which is efficient for large byte sequences. + /// + /// [length] specifies the number of bytes to read. + /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') Uint8List readBytes(int length) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Bytes'); - // Create a view of the underlying buffer without copying. + // Create a view of the underlying buffer without copying final bOffset = _ctx.baseOffset; final bytes = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); @@ -154,6 +240,18 @@ extension type const BinaryReader._(_Reader _ctx) { return bytes; } + /// Reads a UTF-8 encoded string of the specified byte length. + /// + /// [length] is the number of UTF-8 bytes to read (not the number of + /// characters). + /// The string is decoded directly from the buffer without copying. + /// + /// [allowMalformed] controls how invalid UTF-8 sequences are handled: + /// - If true: replaces malformed sequences with U+FFFD (�) + /// - If false (default): throws [FormatException] on invalid UTF-8 + /// + /// Note: This reads a fixed number of bytes. For length-prefixed strings, + /// read the length first (e.g., with [readVarInt]) then call this method. @pragma('vm:prefer-inline') String readString(int length, {bool allowMalformed = false}) { if (length == 0) { @@ -169,6 +267,16 @@ extension type const BinaryReader._(_Reader _ctx) { return utf8.decode(view, allowMalformed: allowMalformed); } + /// Reads bytes without advancing the read position. + /// + /// This allows inspecting upcoming data without consuming it. + /// Useful for protocol parsing where you need to look ahead. + /// + /// [length] specifies the number of bytes to peek at. + /// [offset] specifies where to start peeking (defaults to current position). + /// + /// Returns a view of the buffer without copying data. + /// Asserts bounds in debug mode if peeking past buffer end. @pragma('vm:prefer-inline') Uint8List peekBytes(int length, [int? offset]) { assert(length >= 0, 'Length must be non-negative'); @@ -185,6 +293,12 @@ extension type const BinaryReader._(_Reader _ctx) { return _ctx.data.buffer.asUint8List(bOffset + peekOffset, length); } + /// Advances the read position by the specified number of bytes. + /// + /// This is useful for skipping over data you don't need to process. + /// More efficient than reading and discarding data. + /// + /// Asserts bounds in debug mode if skipping past buffer end. void skip(int length) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Skip'); @@ -192,14 +306,34 @@ extension type const BinaryReader._(_Reader _ctx) { _ctx.offset += length; } + /// Resets the read position to the beginning of the buffer. + /// + /// This allows re-reading the same data without creating a new reader. @pragma('vm:prefer-inline') void reset() { _ctx.offset = 0; } + + /// Internal method to check if enough bytes are available to read. + /// + /// Throws an assertion error in debug mode if not enough bytes. + @pragma('vm:prefer-inline') + void _checkBounds(int bytes, String type, [int? offset]) { + assert( + (offset ?? _ctx.offset) + bytes <= _ctx.length, + 'Not enough bytes to read $type: required $bytes bytes, available ' + '${_ctx.length - _ctx.offset} bytes at offset ${_ctx.offset}', + ); + } } -final class _Reader { - _Reader(Uint8List buffer) +/// Internal state holder for [BinaryReader]. +/// +/// Stores the buffer, read position, and provides efficient typed access +/// through [ByteData]. Separated from the extension type to enable +/// zero-cost abstractions and efficient inline operations. +final class _ReaderState { + _ReaderState(Uint8List buffer) : list = buffer, data = ByteData.sublistView(buffer).asUnmodifiableView(), buffer = buffer.buffer, @@ -207,18 +341,22 @@ final class _Reader { baseOffset = buffer.offsetInBytes, offset = 0; + /// Direct access to the underlying byte list. final Uint8List list; - /// Efficient view for typed data access. + /// Efficient view for typed data access (getInt32, getFloat64, etc.). final ByteData data; + /// The underlying byte buffer. final ByteBuffer buffer; - /// Total length of the buffer. + /// Total length of the buffer in bytes. final int length; /// Current read position in the buffer. late int offset; + /// Offset of the buffer view within its underlying [ByteBuffer]. + /// Necessary for creating accurate subviews. final int baseOffset; } diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index fef3061..0f222cf 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,18 +1,45 @@ import 'dart:typed_data'; -extension type BinaryWriter._(_Writer _ctx) { +/// A high-performance binary writer for encoding data into a byte buffer. +/// +/// Provides methods for writing various data types including: +/// - Variable-length integers (VarInt, ZigZag) +/// - Fixed-width integers (8, 16, 32, 64-bit signed and unsigned) +/// - Floating-point numbers (32 and 64-bit) +/// - Byte arrays +/// - UTF-8 encoded strings +/// +/// The writer automatically expands its internal buffer as needed. +/// +/// Example: +/// ```dart +/// final writer = BinaryWriter(); +/// writer.writeUint32(42); +/// writer.writeString('Hello'); +/// final bytes = writer.takeBytes(); +/// ``` +extension type BinaryWriter._(_WriterState _ctx) { + /// Creates a new [BinaryWriter] with the specified initial buffer size. + /// + /// The buffer will automatically expand as needed when writing data. + /// A larger initial size can improve performance if you know approximately + /// how much data will be written. + /// + /// [initialBufferSize] defaults to 128 bytes. BinaryWriter({int initialBufferSize = 128}) - : this._(_Writer(initialBufferSize)); + : this._(_WriterState(initialBufferSize)); + /// Returns the total number of bytes written to the buffer. int get bytesWritten => _ctx.offset; - @pragma('vm:prefer-inline') - void _checkRange(int value, int min, int max, String typeName) { - if (value < min || value > max) { - throw RangeError.range(value, min, max, typeName); - } - } - + /// Writes a variable-length integer using VarInt encoding. + /// + /// VarInt encoding uses the lower 7 bits of each byte for data and the + /// highest bit as a continuation flag. This is more space-efficient for + /// small numbers (1-5 bytes for typical 32-bit values). + /// + /// Only non-negative integers are supported. For signed integers, use + /// [writeZigZag] instead. @pragma('vm:prefer-inline') void writeVarInt(int value) { // Fast path for single-byte VarInt @@ -37,12 +64,23 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset = offset; } + /// Writes a signed integer using ZigZag encoding followed by VarInt. + /// + /// ZigZag encoding maps signed integers to unsigned integers in a way that + /// small absolute values (both positive and negative) use fewer bytes: + /// - 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. + /// + /// This is more efficient than VarInt for signed values that may be negative. void writeZigZag(int value) { - // Encode zig-zag encoding + // ZigZag: (n << 1) ^ (n >> 63) + // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 final encoded = (value << 1) ^ (value >> 63); writeVarInt(encoded); } + /// Writes an 8-bit unsigned integer (0-255). + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); @@ -51,6 +89,9 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.list[_ctx.offset++] = value; } + /// Writes an 8-bit signed integer (-128 to 127). + /// + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); @@ -59,6 +100,10 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.list[_ctx.offset++] = value & 0xFF; } + /// Writes a 16-bit unsigned integer (0-65535). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); @@ -68,6 +113,10 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 2; } + /// Writes a 16-bit signed integer (-32768 to 32767). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); @@ -77,6 +126,10 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 2; } + /// Writes a 32-bit unsigned integer (0 to 4,294,967,295). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); @@ -86,6 +139,10 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 4; } + /// Writes a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). + /// + /// [endian] specifies byte order (defaults to big-endian). + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); @@ -95,6 +152,11 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 4; } + /// Writes a 64-bit unsigned integer (0 to 9,223,372,036,854,775,807). + /// + /// Note: Dart's integer precision is limited to 2^53 for web targets. + /// [endian] specifies byte order (defaults to big-endian). + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint64(int value, [Endian endian = .big]) { _checkRange(value, 0, 9223372036854775807, 'Uint64'); @@ -104,6 +166,11 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 8; } + /// Writes a 64-bit signed integer. + /// + /// Note: Dart's integer precision is limited to 2^53 for web targets. + /// [endian] specifies byte order (defaults to big-endian). + /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt64(int value, [Endian endian = .big]) { _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); @@ -113,6 +180,9 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 8; } + /// Writes a 32-bit floating-point number (IEEE 754 single precision). + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') void writeFloat32(double value, [Endian endian = .big]) { _ctx.ensureFourBytes(); @@ -120,6 +190,9 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 4; } + /// Writes a 64-bit floating-point number (IEEE 754 double precision). + /// + /// [endian] specifies byte order (defaults to big-endian). @pragma('vm:prefer-inline') void writeFloat64(double value, [Endian endian = .big]) { _ctx.ensureEightBytes(); @@ -127,6 +200,10 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += 8; } + /// Writes a sequence of bytes from the given list. + /// + /// [offset] specifies the starting position in [bytes] (defaults to 0). + /// [length] specifies how many bytes to write (defaults to remaining bytes). @pragma('vm:prefer-inline') void writeBytes(List bytes, [int offset = 0, int? length]) { final len = length ?? (bytes.length - offset); @@ -136,6 +213,19 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset += len; } + /// Writes a UTF-8 encoded string. + /// + /// The string is encoded directly to UTF-8 bytes with optimized handling for: + /// - ASCII fast path (unrolled loops for better performance) + /// - Multi-byte UTF-8 sequences (Cyrillic, CJK, emojis, etc.) + /// - Proper surrogate pair handling for characters outside the BMP + /// + /// [allowMalformed] controls how invalid UTF-16 sequences are handled: + /// - If true (default): replaces lone surrogates with U+FFFD (�) + /// - If false: throws [FormatException] on malformed input + /// + /// Note: This does NOT write the string length. For length-prefixed strings, + /// call [writeVarInt] with the length before calling this method. @pragma('vm:prefer-inline') void writeString(String value, {bool allowMalformed = true}) { final len = value.length; @@ -143,8 +233,9 @@ extension type BinaryWriter._(_Writer _ctx) { return; } - // Pre-allocate: worst case for UTF-16 to UTF-8 is 3 bytes per code unit. - // (Surrogate pairs are 2 units -> 4 bytes, which is 2 bytes/unit). + // Pre-allocate buffer: worst case is 3 bytes per UTF-16 code unit + // Most common case: 1 byte/char (ASCII) or 2-3 bytes/char (non-ASCII) + // Surrogate pairs: 2 units -> 4 bytes UTF-8 (2 bytes per unit average) _ctx.ensureSize(len * 3); final list = _ctx.list; @@ -156,12 +247,15 @@ extension type BinaryWriter._(_Writer _ctx) { if (c < 0x80) { // ------------------------------------------------------- - // ASCII Fast Path + // ASCII Fast Path: Optimized for common case + // Most strings contain primarily ASCII, so we optimize this path + // with unrolled loops to process 4 characters at a time. // ------------------------------------------------------- list[offset++] = c; i++; - // Unrolled loop for blocks of 4 ASCII characters + // Unrolled loop: process 4 ASCII chars at once + // Bitwise OR (|) checks if any char >= 0x80 in one operation while (i <= len - 4) { final c0 = value.codeUnitAt(i); final c1 = value.codeUnitAt(i + 1); @@ -196,23 +290,31 @@ extension type BinaryWriter._(_Writer _ctx) { } // ------------------------------------------------------- - // Multi-byte handling + // Multi-byte UTF-8 encoding + // UTF-8 uses 2-4 bytes for non-ASCII characters // ------------------------------------------------------- if (c < 0x800) { - // 2 bytes: Cyrillic, Greek, Arabic, etc. + // 2-byte sequence: U+0080 to U+07FF + // Covers: Latin Extended, Greek, Cyrillic, Arabic, Hebrew, etc. list[offset++] = 0xC0 | (c >> 6); list[offset++] = 0x80 | (c & 0x3F); i++; } else if (c < 0xD800 || c > 0xDFFF) { - // 3 bytes: Basic Multilingual Plane + // 3-byte sequence: U+0800 to U+FFFF (excluding surrogates) + // Covers: CJK characters, most world scripts, symbols, etc. list[offset++] = 0xE0 | (c >> 12); list[offset++] = 0x80 | ((c >> 6) & 0x3F); list[offset++] = 0x80 | (c & 0x3F); i++; } else if (c <= 0xDBFF && i + 1 < len) { - // 4 bytes: Valid Surrogate Pair + // 4-byte sequence: U+10000 to U+10FFFF via surrogate pairs + // High surrogate (0xD800-0xDBFF) must be followed by low + // (0xDC00-0xDFFF) + // Covers: Emojis, historic scripts, rare CJK, musical notation, etc. final next = value.codeUnitAt(i + 1); if (next >= 0xDC00 && next <= 0xDFFF) { + // Valid surrogate pair: combine high and low surrogates + // Formula: 0x10000 + ((high & 0x3FF) << 10) + (low & 0x3FF) final codePoint = 0x10000 + ((c & 0x3FF) << 10) + (next & 0x3FF); list[offset++] = 0xF0 | (codePoint >> 18); list[offset++] = 0x80 | ((codePoint >> 12) & 0x3F); @@ -220,11 +322,12 @@ extension type BinaryWriter._(_Writer _ctx) { list[offset++] = 0x80 | (codePoint & 0x3F); i += 2; } else { + // Invalid: high surrogate not followed by low surrogate offset = _handleMalformed(value, i, offset, allowMalformed); i++; } } else { - // Malformed: Lone surrogate or end of string + // Malformed UTF-16: lone low surrogate or high surrogate at end offset = _handleMalformed(value, i, offset, allowMalformed); i++; } @@ -233,11 +336,41 @@ extension type BinaryWriter._(_Writer _ctx) { _ctx.offset = offset; } + /// Extracts all written bytes and resets the writer. + /// + /// After calling this method, the writer is reset and ready for reuse. + /// This is more efficient than creating a new writer for each operation. + /// + /// Returns a view of the written bytes (no copying occurs). + @pragma('vm:prefer-inline') + Uint8List takeBytes() { + final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); + _ctx._initializeBuffer(); + return result; + } + + /// Returns a view of the written bytes without resetting the writer. + /// + /// Unlike [takeBytes], this does not reset the writer's state. + /// Subsequent writes will continue appending to the buffer. + @pragma('vm:prefer-inline') + Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); + + /// Resets the writer to its initial state, discarding all written data. + @pragma('vm:prefer-inline') + void reset() => _ctx._initializeBuffer(); + + /// Handles malformed UTF-16 sequences (lone surrogates). + /// + /// If [allow] is false, throws [FormatException]. + /// Otherwise, writes the Unicode replacement character U+FFFD (�) + /// encoded as UTF-8: 0xEF 0xBF 0xBD @pragma('vm:prefer-inline') int _handleMalformed(String v, int i, int offset, bool allow) { if (!allow) { throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); } + // Write UTF-8 encoding of U+FFFD replacement character (�) final list = _ctx.list; list[offset] = 0xEF; list[offset + 1] = 0xBF; @@ -246,21 +379,19 @@ extension type BinaryWriter._(_Writer _ctx) { } @pragma('vm:prefer-inline') - Uint8List takeBytes() { - final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); - _ctx._initializeBuffer(); - return result; + void _checkRange(int value, int min, int max, String typeName) { + if (value < min || value > max) { + throw RangeError.range(value, min, max, typeName); + } } - - @pragma('vm:prefer-inline') - Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); - - @pragma('vm:prefer-inline') - void reset() => _ctx._initializeBuffer(); } -final class _Writer { - _Writer(int initialBufferSize) +/// Internal state holder for [BinaryWriter]. +/// +/// Manages the underlying buffer, capacity tracking, and expansion logic. +/// Separated from the extension type to allow efficient inline operations. +final class _WriterState { + _WriterState(int initialBufferSize) : _size = initialBufferSize, capacity = initialBufferSize, offset = 0, @@ -337,9 +468,15 @@ final class _Writer { _expand(8); } + /// Expands the buffer to accommodate additional data. + /// + /// Uses exponential growth (2x) for better amortized performance, + /// but ensures the buffer is always large enough for the requested size. void _expand(int size) { final req = offset + size; + // Double the capacity (exponential growth) var newCapacity = capacity * 2; + // Ensure we meet the minimum requirement if (newCapacity < req) { newCapacity = req; } From e597bf5d42ea9a90046bf74079f40c311ee9e6f2 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 25 Dec 2025 14:21:56 +0200 Subject: [PATCH 06/29] Refactor VarInt methods to clarify unsigned and signed usage in BinaryReader and BinaryWriter --- lib/src/binary_reader.dart | 17 ++++--- lib/src/binary_writer.dart | 22 ++++---- test/binary_reader_test.dart | 98 ++++++++++++++++++------------------ test/binary_writer_test.dart | 30 +++++------ 4 files changed, 86 insertions(+), 81 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 7e6412b..a97b464 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -37,21 +37,22 @@ extension type const BinaryReader._(_ReaderState _ctx) { @pragma('vm:prefer-inline') int get length => _ctx.length; - /// Reads a variable-length integer encoded using VarInt format. + /// Reads an unsigned variable-length integer encoded using VarInt format. /// /// VarInt encoding uses the lower 7 bits of each byte for data and the /// highest bit as a continuation flag. This format is space-efficient - /// for small numbers (1-5 bytes for typical 32-bit values). + /// for small unsigned numbers (1-5 bytes for typical 32-bit values). /// /// The algorithm: /// 1. Read a byte and extract the lower 7 bits /// 2. If the 8th bit is set, continue reading /// 3. Shift and combine all 7-bit chunks /// + /// For signed integers with efficient negative encoding, use [readVarInt]. /// Throws [FormatException] if the VarInt exceeds 10 bytes (malformed data). /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') - int readVarInt() { + int readVarUint() { var result = 0; var shift = 0; @@ -78,17 +79,18 @@ extension type const BinaryReader._(_ReaderState _ctx) { throw const FormatException('VarInt is too long (more than 10 bytes)'); } - /// Reads a ZigZag-encoded signed integer. + /// Reads a signed variable-length integer using ZigZag decoding. /// /// ZigZag encoding maps signed integers to unsigned values such that /// small absolute values (both positive and negative) use fewer bytes: /// - 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. /// + /// First reads an unsigned VarInt, then applies ZigZag decoding. /// Decoding formula: (n >>> 1) ^ -(n & 1) /// This reverses the encoding: (n << 1) ^ (n >> 63) @pragma('vm:prefer-inline') - int readZigZag() { - final v = readVarInt(); + int readVarInt() { + final v = readVarUint(); // Decode: right shift by 1, XOR with sign-extended LSB return (v >>> 1) ^ -(v & 1); } @@ -251,7 +253,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// - If false (default): throws [FormatException] on invalid UTF-8 /// /// Note: This reads a fixed number of bytes. For length-prefixed strings, - /// read the length first (e.g., with [readVarInt]) then call this method. + /// first read the byte length (e.g., with [readVarUint] or [readVarInt]), + /// then call this method. @pragma('vm:prefer-inline') String readString(int length, {bool allowMalformed = false}) { if (length == 0) { diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 0f222cf..734793a 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -32,16 +32,16 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Returns the total number of bytes written to the buffer. int get bytesWritten => _ctx.offset; - /// Writes a variable-length integer using VarInt encoding. + /// Writes an unsigned variable-length integer using VarInt encoding. /// /// VarInt encoding uses the lower 7 bits of each byte for data and the /// highest bit as a continuation flag. This is more space-efficient for - /// small numbers (1-5 bytes for typical 32-bit values). + /// small unsigned numbers (1-5 bytes for typical 32-bit values). /// - /// Only non-negative integers are supported. For signed integers, use - /// [writeZigZag] instead. + /// Only non-negative integers are supported. For signed integers with + /// efficient negative number encoding, use [writeVarInt] instead. @pragma('vm:prefer-inline') - void writeVarInt(int value) { + void writeVarUint(int value) { // Fast path for single-byte VarInt if (value < 0x80 && value >= 0) { _ctx.ensureOneByte(); @@ -64,18 +64,19 @@ extension type BinaryWriter._(_WriterState _ctx) { _ctx.offset = offset; } - /// Writes a signed integer using ZigZag encoding followed by VarInt. + /// Writes a signed variable-length integer using ZigZag encoding. /// /// ZigZag encoding maps signed integers to unsigned integers in a way that /// small absolute values (both positive and negative) use fewer bytes: /// - 0 => 0, -1 => 1, 1 => 2, -2 => 3, 2 => 4, etc. /// - /// This is more efficient than VarInt for signed values that may be negative. - void writeZigZag(int value) { + /// The encoded value is then written using VarInt format. This is more + /// efficient than [writeVarUint] for signed values that may be negative. + void writeVarInt(int value) { // ZigZag: (n << 1) ^ (n >> 63) // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 final encoded = (value << 1) ^ (value >> 63); - writeVarInt(encoded); + writeVarUint(encoded); } /// Writes an 8-bit unsigned integer (0-255). @@ -225,7 +226,8 @@ extension type BinaryWriter._(_WriterState _ctx) { /// - If false: throws [FormatException] on malformed input /// /// Note: This does NOT write the string length. For length-prefixed strings, - /// call [writeVarInt] with the length before calling this method. + /// first call [writeVarUint] or [writeVarInt] with the byte length, then + /// call this method. @pragma('vm:prefer-inline') void writeString(String value, {bool allowMalformed = true}) { final len = value.length; diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index d25f67d..1a307a2 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -214,7 +214,7 @@ void main() { final buffer = Uint8List.fromList([0]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(0)); + expect(reader.readVarUint(), equals(0)); expect(reader.availableBytes, equals(0)); }); @@ -222,7 +222,7 @@ void main() { final buffer = Uint8List.fromList([127]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(127)); + expect(reader.readVarUint(), equals(127)); expect(reader.availableBytes, equals(0)); }); @@ -230,7 +230,7 @@ void main() { final buffer = Uint8List.fromList([0x80, 0x01]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(128)); + expect(reader.readVarUint(), equals(128)); expect(reader.availableBytes, equals(0)); }); @@ -238,7 +238,7 @@ void main() { final buffer = Uint8List.fromList([0xAC, 0x02]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(300)); + expect(reader.readVarUint(), equals(300)); expect(reader.availableBytes, equals(0)); }); @@ -246,7 +246,7 @@ void main() { final buffer = Uint8List.fromList([0x80, 0x80, 0x01]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(16384)); + expect(reader.readVarUint(), equals(16384)); expect(reader.availableBytes, equals(0)); }); @@ -254,7 +254,7 @@ void main() { final buffer = Uint8List.fromList([0xFF, 0xFF, 0x7F]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(2097151)); + expect(reader.readVarUint(), equals(2097151)); expect(reader.availableBytes, equals(0)); }); @@ -262,7 +262,7 @@ void main() { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0x7F]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(268435455)); + expect(reader.readVarUint(), equals(268435455)); expect(reader.availableBytes, equals(0)); }); @@ -270,32 +270,32 @@ void main() { final buffer = Uint8List.fromList([0x80, 0x80, 0x80, 0x80, 0x04]); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(1 << 30)); + expect(reader.readVarUint(), equals(1 << 30)); expect(reader.availableBytes, equals(0)); }); test('readVarInt roundtrip with writeVarInt', () { final writer = BinaryWriter() - ..writeVarInt(0) - ..writeVarInt(1) - ..writeVarInt(127) - ..writeVarInt(128) - ..writeVarInt(300) - ..writeVarInt(70000) - ..writeVarInt(1 << 20) - ..writeVarInt(1 << 30); + ..writeVarUint(0) + ..writeVarUint(1) + ..writeVarUint(127) + ..writeVarUint(128) + ..writeVarUint(300) + ..writeVarUint(70000) + ..writeVarUint(1 << 20) + ..writeVarUint(1 << 30); final buffer = writer.takeBytes(); final reader = BinaryReader(buffer); - expect(reader.readVarInt(), equals(0)); - expect(reader.readVarInt(), equals(1)); - expect(reader.readVarInt(), equals(127)); - expect(reader.readVarInt(), equals(128)); - expect(reader.readVarInt(), equals(300)); - expect(reader.readVarInt(), equals(70000)); - expect(reader.readVarInt(), equals(1 << 20)); - expect(reader.readVarInt(), equals(1 << 30)); + expect(reader.readVarUint(), equals(0)); + expect(reader.readVarUint(), equals(1)); + expect(reader.readVarUint(), equals(127)); + expect(reader.readVarUint(), equals(128)); + expect(reader.readVarUint(), equals(300)); + expect(reader.readVarUint(), equals(70000)); + expect(reader.readVarUint(), equals(1 << 20)); + expect(reader.readVarUint(), equals(1 << 30)); expect(reader.availableBytes, equals(0)); }); @@ -303,7 +303,7 @@ void main() { final buffer = Uint8List.fromList([0]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(0)); + expect(reader.readVarInt(), equals(0)); expect(reader.availableBytes, equals(0)); }); @@ -311,7 +311,7 @@ void main() { final buffer = Uint8List.fromList([2]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(1)); + expect(reader.readVarInt(), equals(1)); expect(reader.availableBytes, equals(0)); }); @@ -319,7 +319,7 @@ void main() { final buffer = Uint8List.fromList([1]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(-1)); + expect(reader.readVarInt(), equals(-1)); expect(reader.availableBytes, equals(0)); }); @@ -327,7 +327,7 @@ void main() { final buffer = Uint8List.fromList([4]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(2)); + expect(reader.readVarInt(), equals(2)); expect(reader.availableBytes, equals(0)); }); @@ -335,7 +335,7 @@ void main() { final buffer = Uint8List.fromList([3]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(-2)); + expect(reader.readVarInt(), equals(-2)); expect(reader.availableBytes, equals(0)); }); @@ -343,7 +343,7 @@ void main() { final buffer = Uint8List.fromList([0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(2147483647)); + expect(reader.readVarInt(), equals(2147483647)); expect(reader.availableBytes, equals(0)); }); @@ -351,34 +351,34 @@ void main() { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(-2147483648)); + expect(reader.readVarInt(), equals(-2147483648)); expect(reader.availableBytes, equals(0)); }); test('readZigZag roundtrip with writeZigZag', () { final writer = BinaryWriter() - ..writeZigZag(0) - ..writeZigZag(1) - ..writeZigZag(-1) - ..writeZigZag(2) - ..writeZigZag(-2) - ..writeZigZag(100) - ..writeZigZag(-100) - ..writeZigZag(2147483647) - ..writeZigZag(-2147483648); + ..writeVarInt(0) + ..writeVarInt(1) + ..writeVarInt(-1) + ..writeVarInt(2) + ..writeVarInt(-2) + ..writeVarInt(100) + ..writeVarInt(-100) + ..writeVarInt(2147483647) + ..writeVarInt(-2147483648); final buffer = writer.takeBytes(); final reader = BinaryReader(buffer); - expect(reader.readZigZag(), equals(0)); - expect(reader.readZigZag(), equals(1)); - expect(reader.readZigZag(), equals(-1)); - expect(reader.readZigZag(), equals(2)); - expect(reader.readZigZag(), equals(-2)); - expect(reader.readZigZag(), equals(100)); - expect(reader.readZigZag(), equals(-100)); - expect(reader.readZigZag(), equals(2147483647)); - expect(reader.readZigZag(), equals(-2147483648)); + expect(reader.readVarInt(), equals(0)); + expect(reader.readVarInt(), equals(1)); + expect(reader.readVarInt(), equals(-1)); + expect(reader.readVarInt(), equals(2)); + expect(reader.readVarInt(), equals(-2)); + expect(reader.readVarInt(), equals(100)); + expect(reader.readVarInt(), equals(-100)); + expect(reader.readVarInt(), equals(2147483647)); + expect(reader.readVarInt(), equals(-2147483648)); expect(reader.availableBytes, equals(0)); }); diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 5c9e4fa..2fbd391 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -106,77 +106,77 @@ void main() { }); test('should write VarInt single byte (0)', () { - writer.writeVarInt(0); + writer.writeVarUint(0); expect(writer.takeBytes(), [0]); }); test('should write VarInt single byte (127)', () { - writer.writeVarInt(127); + writer.writeVarUint(127); expect(writer.takeBytes(), [127]); }); test('should write VarInt two bytes (128)', () { - writer.writeVarInt(128); + writer.writeVarUint(128); expect(writer.takeBytes(), [0x80, 0x01]); }); test('should write VarInt two bytes (300)', () { - writer.writeVarInt(300); + writer.writeVarUint(300); expect(writer.takeBytes(), [0xAC, 0x02]); }); test('should write VarInt three bytes (16384)', () { - writer.writeVarInt(16384); + writer.writeVarUint(16384); expect(writer.takeBytes(), [0x80, 0x80, 0x01]); }); test('should write VarInt four bytes (2097151)', () { - writer.writeVarInt(2097151); + writer.writeVarUint(2097151); expect(writer.takeBytes(), [0xFF, 0xFF, 0x7F]); }); test('should write VarInt five bytes (268435455)', () { - writer.writeVarInt(268435455); + writer.writeVarUint(268435455); expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0x7F]); }); test('should write VarInt large value', () { - writer.writeVarInt(1 << 30); + writer.writeVarUint(1 << 30); expect(writer.takeBytes(), [0x80, 0x80, 0x80, 0x80, 0x04]); }); test('should write ZigZag encoding for positive values', () { - writer.writeZigZag(0); + writer.writeVarInt(0); expect(writer.takeBytes(), [0]); }); test('should write ZigZag encoding for positive value 1', () { - writer.writeZigZag(1); + writer.writeVarInt(1); expect(writer.takeBytes(), [2]); }); test('should write ZigZag encoding for negative value -1', () { - writer.writeZigZag(-1); + writer.writeVarInt(-1); expect(writer.takeBytes(), [1]); }); test('should write ZigZag encoding for positive value 2', () { - writer.writeZigZag(2); + writer.writeVarInt(2); expect(writer.takeBytes(), [4]); }); test('should write ZigZag encoding for negative value -2', () { - writer.writeZigZag(-2); + writer.writeVarInt(-2); expect(writer.takeBytes(), [3]); }); test('should write ZigZag encoding for large positive value', () { - writer.writeZigZag(2147483647); + writer.writeVarInt(2147483647); expect(writer.takeBytes(), [0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); }); test('should write ZigZag encoding for large negative value', () { - writer.writeZigZag(-2147483648); + writer.writeVarInt(-2147483648); expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); }); From cce071f1fe7effeff89e6c0f38ceb4aab88df037 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 25 Dec 2025 16:31:22 +0200 Subject: [PATCH 07/29] feat: Introduce VarBytes and VarString methods in BinaryWriter and BinaryReader - Added `writeVarBytes` and `readVarBytes` methods for handling length-prefixed byte arrays. - Introduced `writeVarString` and `readVarString` methods for length-prefixed UTF-8 encoded strings. - Implemented `getUtf8Length` function to calculate the UTF-8 byte length of strings without encoding. - Enhanced performance tests for BinaryReader and BinaryWriter to include benchmarks for new methods. - Updated existing tests to cover new functionality and ensure correctness. - Bumped version to 3.0.0 to reflect breaking changes and new features. --- CHANGELOG.md | 15 ++ README.md | 289 ++++++++++++--------- lib/src/binary_reader.dart | 193 +++++++++++++- lib/src/binary_writer.dart | 305 ++++++++++++++++++++++- pubspec.yaml | 2 +- test/binary_reader_performance_test.dart | 107 +++++++- test/binary_reader_test.dart | 191 ++++++++++++++ test/binary_writer_test.dart | 227 +++++++++++++++++ 8 files changed, 1187 insertions(+), 142 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f9aeef..09e70d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 3.0.0 + +**Improvements:** + +- **docs**: Comprehensive documentation overhaul + - Added detailed API documentation with usage examples for all methods + - Documented `writeVarString()`, `readVarString()`, and `getUtf8Length()` + - Included performance notes and best practices + - Added inline comments explaining complex encoding algorithms + - Improved README with real-world examples and migration guide +- **test**: Added 23 new comprehensive tests + - 7 tests for `writeVarString()` (ASCII, UTF-8, emoji, empty, mixed, round-trip, malformed) + - 8 tests for `getUtf8Length()` (ASCII, empty, 2-byte, 3-byte, 4-byte, mixed, validation, surrogates) + - 8 tests for `readVarString()` (basic, UTF-8, emoji, empty, multiple, error handling) + ## 2.2.0 **test**: Added integration tests for new error handling features diff --git a/README.md b/README.md index 3fcb230..4cd8ba9 100644 --- a/README.md +++ b/README.md @@ -4,171 +4,234 @@ [![Tests](https://github.com/pro100andrey/pro_binary/workflows/Tests/badge.svg)](https://github.com/pro100andrey/pro_binary/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -Efficient binary serialization library for Dart with comprehensive boundary checks and detailed error messages. +High-performance binary serialization for Dart. Fast, type-safe, and easy to use. -## Features +## Why pro_binary? -- ✅ Read/write operations for all primitive types (int8/16/32/64, uint8/16/32/64, float32/64) -- ✅ Big-endian and little-endian support -- ✅ Comprehensive boundary checks with detailed error messages -- ✅ UTF-8 string encoding with multibyte character support -- ✅ Dynamic buffer resizing with efficient memory management -- ✅ Zero-copy operations where possible +- 🚀 **Fast**: Optimized for performance with zero-copy operations +- 🎯 **Type-safe**: Full support for all Dart primitive types +- 🔍 **Developer-friendly**: Clear error messages in debug mode +- 📦 **Smart**: Auto-expanding buffers, VarInt encoding for smaller payloads +- 🌐 **Flexible**: Big-endian and little-endian support ## Installation -Add this to your package's `pubspec.yaml` file: - -``` yaml +```yaml dependencies: - pro_binary: ^2.1.0 + pro_binary: ^3.0.0 ``` -Then, run `pub get` to install the package. - ## Quick Start -### Writing - ```dart import 'package:pro_binary/pro_binary.dart'; -void main() { - final writer = BinaryWriter() - ..writeUint8(42) - ..writeUint32(1000000, .little) - ..writeFloat64(3.14159) - ..writeString('Hello'); +// Writing data +final writer = BinaryWriter(); +writer.writeUint32(42); +writer.writeString('Hello, World!'); +final bytes = writer.takeBytes(); - final bytes = writer.takeBytes(); - print('Written ${bytes.length} bytes'); -} +// Reading data +final reader = BinaryReader(bytes); +final number = reader.readUint32(); // 42 +final text = reader.readString(13); // 'Hello, World!' ``` -### Reading +## Core API -```dart -import 'dart:typed_data'; -import 'package:pro_binary/pro_binary.dart'; +### Writing Data -void main() { - final data = Uint8List.fromList([42, 64, 66, 15, 0]); - final reader = BinaryReader(data); +```dart +final writer = BinaryWriter(); - final value1 = reader.readUint8(); // 42 - final value2 = reader.readUint32(.little); // 1000000 - - print('Read: $value1, $value2'); - print('Remaining: ${reader.availableBytes} bytes'); -} -``` +// Integers (8, 16, 32, 64-bit signed/unsigned) +writer.writeUint8(255); +writer.writeInt32(-1000, Endian.little); +writer.writeUint64(9999999); -## API Overview +// Floats +writer.writeFloat32(3.14); +writer.writeFloat64(3.14159265359); -### BinaryWriter +// Variable-length integers (space-efficient!) +writer.writeVarUint(42); // Unsigned VarInt +writer.writeVarInt(-42); // Signed VarInt with ZigZag -```dart -final writer = BinaryWriter(initialBufferSize: 64); +// Strings +writer.writeString('text'); // Fixed UTF-8 string (you control length) +writer.writeVarString('Hello'); // Length-prefixed UTF-8 string (auto length) -// Write operations -writer.writeUint8(255); -writer.writeInt8(-128); -writer.writeUint16(65535, .big); -writer.writeInt16(-32768, .big); -writer.writeUint32(4294967295, .big); -writer.writeInt32(-1000, .big); -writer.writeUint64(9223372036854775807, .big); -writer.writeInt64(-9223372036854775808, .big); -writer.writeFloat32(3.14, .big); -writer.writeFloat64(3.14159, .big); -writer.writeBytes([1, 2, 3]); -writer.writeString('text'); - -// Buffer operations -final bytes = writer.toBytes(); // Get view without reset -final result = writer.takeBytes(); // Get view and reset -writer.reset(); // Reset without returning -print(writer.bytesWritten); // Check written size +// Get result +final bytes = writer.takeBytes(); // Gets bytes and resets +// or +final view = writer.toBytes(); // Gets bytes, keeps state ``` -### BinaryReader +### Reading Data ```dart -final reader = BinaryReader(buffer); +final reader = BinaryReader(bytes); -// Read operations +// Read primitives (matching write order) final u8 = reader.readUint8(); -final i8 = reader.readInt8(); -final u16 = reader.readUint16(.big); -final i16 = reader.readInt16(.big); -final u32 = reader.readUint32(.big); -final i32 = reader.readInt32(.little); -final u64 = reader.readUint64(.big); -final i64 = reader.readInt64(.big); -final f32 = reader.readFloat32(.big); -final f64 = reader.readFloat64(.big); -final bytes = reader.readBytes(10); -final text = reader.readString(5); - -// Peek without advancing position -final peeked = reader.peekBytes(4); // View without consuming +final i32 = reader.readInt32(Endian.little); +final f64 = reader.readFloat64(); + +// Variable-length integers +final count = reader.readVarUint(); +final delta = reader.readVarInt(); + +// Strings +final text = reader.readString(10); // Read 10 UTF-8 bytes (you specify length) +final message = reader.readVarString(); // Read length-prefixed string (auto length) // Navigation -reader.skip(4); // Skip bytes -final pos = reader.offset; // Current position -final used = reader.usedBytes; // Bytes read so far -reader.reset(); // Reset to start -print(reader.availableBytes); // Remaining bytes +reader.skip(4); // Skip bytes +final peek = reader.peekBytes(2); // Look ahead without consuming +reader.reset(); // Go back to start + +// Check state +print(reader.offset); // Current position +print(reader.availableBytes); // Bytes left to read ``` -## Error Handling +## Real-World Examples -All read operations validate boundaries and provide detailed error messages: +### Protocol Messages ```dart -try { - reader.readUint32(); // Not enough bytes -} catch (e) { - // AssertionError: Not enough bytes to read Uint32: - // required 4 bytes, available 2 bytes at offset 10 +// Encode message +final writer = BinaryWriter(); +writer.writeUint8(0x42); // Message type +writer.writeVarUint(payload.length); +writer.writeBytes(payload); +sendToServer(writer.takeBytes()); + +// Decode message +final reader = BinaryReader(received); +final type = reader.readUint8(); +final length = reader.readVarUint(); +final payload = reader.readBytes(length); +``` + +### Length-Prefixed Strings + +```dart +// Write +final text = 'Hello, 世界! 🌍'; +final encoded = utf8.encode(text); +writer.writeVarUint(encoded.length); +writer.writeString(text); + +// Read +final length = reader.readVarUint(); +final text = reader.readString(length); +``` + +### Struct-like Data + +```dart +class Player { + final int id; + final String name; + final double x, y; + + void writeTo(BinaryWriter w) { + w.writeUint32(id); + final nameBytes = utf8.encode(name); + w.writeVarUint(nameBytes.length); + w.writeString(name); + w.writeFloat64(x); + w.writeFloat64(y); + } + + static Player readFrom(BinaryReader r) { + final id = r.readUint32(); + final nameLen = r.readVarUint(); + final name = r.readString(nameLen); + final x = r.readFloat64(); + final y = r.readFloat64(); + return Player(id, name, x, y); + } } ``` -## Contributing +## VarInt Encoding -Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on: +VarInt uses fewer bytes for small numbers: -- How to set up the development environment -- Running tests and coverage -- Code style and formatting -- Submitting pull requests +```dart +writer.writeVarUint(42); // 1 byte (vs 4 for Uint32) +writer.writeVarUint(300); // 2 bytes +writer.writeVarUint(1000000); // 3 bytes -For bugs and features, use the [issue templates](https://github.com/pro100andrey/pro_binary/issues/new/choose). +writer.writeVarInt(-1); // 1 byte (ZigZag encoded) +writer.writeVarInt(-1000); // 2 bytes +``` -## Testing +**Use VarUint** for: lengths, counts, IDs +**Use VarInt** for: deltas, offsets, signed values -The library includes comprehensive test coverage with **279+ tests** covering: +## Tips & Best Practices -- **Basic operations**: All read/write methods for each data type -- **Endianness**: Big-endian and little-endian operations -- **Edge cases**: Boundary conditions, overflow, special values (NaN, Infinity) -- **UTF-8 handling**: Multi-byte characters, emoji, malformed sequences -- **Buffer management**: Expansion, growth strategy, memory efficiency -- **Integration tests**: Complete read-write cycles and round-trip validation -- **Performance tests**: Benchmark measurements for optimization +**Buffer Sizing**: Writer starts at 128 bytes and auto-expands. For large data, set initial size: -Run tests with: +```dart +final writer = BinaryWriter(initialBufferSize: 1024); +``` -```bash -dart test +**Endianness**: Defaults to big-endian. Specify when needed: + +```dart +writer.writeUint32(value, Endian.little); +``` + +**String Encoding**: Always use length-prefix for variable strings: + +```dart +// ✅ Good +final bytes = utf8.encode(text); +writer.writeVarUint(bytes.length); +writer.writeString(text); + +// ❌ Bad - no way to know where string ends +writer.writeString(text); +``` + +**Error Handling**: Bounds checks run in debug mode. Catch errors for user input: + +```dart +try { + final value = reader.readUint32(); +} catch (e) { + print('Invalid data: $e'); +} ``` -Analyze code quality: +## Testing + +Comprehensive test suite with **336+ tests** covering: + +- ✅ **VarInt/VarUint encoding** - 70+ dedicated tests for variable-length integers +- ✅ **All data types** - Exhaustive testing of read/write operations +- ✅ **Edge cases** - Boundary conditions, overflow, special values +- ✅ **UTF-8 handling** - Multi-byte characters, emojis, malformed sequences +- ✅ **Round-trip validation** - Ensures data integrity through encode/decode cycles +- ✅ **Performance benchmarks** - Tracks optimization effectiveness + +Run tests: ```bash -dart analyze +dart test # Run all tests +dart test test/varint_test.dart # Run VarInt-specific tests +dart analyze # Check code quality ``` +## Contributing + +Found a bug or have a feature idea? [Open an issue](https://github.com/pro100andrey/pro_binary/issues) or submit a PR! + ## License -This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more details. +MIT License - see [LICENSE](./LICENSE) for details. diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index a97b464..bc662b1 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -1,6 +1,9 @@ import 'dart:convert'; import 'dart:typed_data'; +import '../pro_binary.dart' show BinaryWriter; +import 'binary_writer.dart' show BinaryWriter; + /// A high-performance binary reader for decoding data from a byte buffer. /// /// Provides methods for reading various data types including: @@ -15,8 +18,14 @@ import 'dart:typed_data'; /// Example: /// ```dart /// final reader = BinaryReader(bytes); -/// final value = reader.readUint32(); -/// final text = reader.readString(10); +/// // Read various data types +/// final id = reader.readUint32(); +/// final value = reader.readFloat64(); +/// // Read length-prefixed string +/// final stringLength = reader.readVarUint(); +/// final text = reader.readString(stringLength); +/// // Check remaining data +/// print('Bytes left: ${reader.availableBytes}'); /// ``` extension type const BinaryReader._(_ReaderState _ctx) { /// Creates a new [BinaryReader] from the given byte buffer. @@ -48,7 +57,19 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// 2. If the 8th bit is set, continue reading /// 3. Shift and combine all 7-bit chunks /// - /// For signed integers with efficient negative encoding, use [readVarInt]. + /// **Use this for:** Lengths, counts, sizes, unsigned IDs. + /// + /// For signed integers (especially with negative values), use [readVarInt] + /// which uses ZigZag decoding for better compression of negative numbers. + /// + /// Example: + /// ```dart + /// final count = reader.readVarUint(); // Read array length + /// for (var i = 0; i < count; i++) { + /// // Process array elements + /// } + /// ``` + /// /// Throws [FormatException] if the VarInt exceeds 10 bytes (malformed data). /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') @@ -88,6 +109,14 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// First reads an unsigned VarInt, then applies ZigZag decoding. /// Decoding formula: (n >>> 1) ^ -(n & 1) /// This reverses the encoding: (n << 1) ^ (n >> 63) + /// + /// **Use this for:** Signed values, deltas, offsets, coordinates. + /// + /// Example: + /// ```dart + /// final delta = reader.readVarInt(); // Can be positive or negative + /// final position = lastPosition + delta; + /// ``` @pragma('vm:prefer-inline') int readVarInt() { final v = readVarUint(); @@ -97,6 +126,11 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads an 8-bit unsigned integer (0-255). /// + /// Example: + /// ```dart + /// final version = reader.readUint8(); // Protocol version + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint8() { @@ -107,6 +141,11 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads an 8-bit signed integer (-128 to 127). /// + /// Example: + /// ```dart + /// final offset = reader.readInt8(); // Small delta value + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt8() { @@ -118,6 +157,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 16-bit unsigned integer (0-65535). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final port = reader.readUint16(); // Network port number + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint16([Endian endian = .big]) { @@ -132,6 +177,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 16-bit signed integer (-32768 to 32767). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final temperature = reader.readInt16(); // -100 to 100°C + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt16([Endian endian = .big]) { @@ -146,6 +197,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 32-bit unsigned integer (0 to 4,294,967,295). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final timestamp = reader.readUint32(); // Unix timestamp + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint32([Endian endian = .big]) { @@ -159,6 +216,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final coordinate = reader.readInt32(); // GPS coordinate + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt32([Endian endian = .big]) { @@ -171,7 +234,14 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 64-bit unsigned integer. /// /// Note: Dart's integer precision is limited to 2^53 on web targets. + /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final id = reader.readUint64(); // Large unique identifier + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readUint64([Endian endian = .big]) { @@ -184,7 +254,14 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 64-bit signed integer. /// /// Note: Dart's integer precision is limited to 2^53 on web targets. + /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final nanoseconds = reader.readInt64(); // High-precision time + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') int readInt64([Endian endian = .big]) { @@ -197,6 +274,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 32-bit floating-point number (IEEE 754 single precision). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final temperature = reader.readFloat32(); // 25.5°C + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') double readFloat32([Endian endian = .big]) { @@ -211,6 +294,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Reads a 64-bit floating-point number (IEEE 754 double precision). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// final price = reader.readFloat64(); // $123.45 + /// ``` + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') double readFloat64([Endian endian = .big]) { @@ -227,6 +316,15 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// which is efficient for large byte sequences. /// /// [length] specifies the number of bytes to read. + /// + /// Example: + /// ```dart + /// final header = reader.readBytes(4); // Read 4-byte header + /// final payload = reader.readBytes(256); // Read payload + /// ``` + /// + /// **Performance:** Zero-copy operation using buffer views. + /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') Uint8List readBytes(int length) { @@ -242,19 +340,55 @@ extension type const BinaryReader._(_ReaderState _ctx) { return bytes; } + /// Reads a length-prefixed byte array. + /// + /// First reads the length as a VarUint, then reads that many bytes. + /// Returns a view of the underlying buffer without copying data. + /// + /// This is the counterpart to [BinaryWriter.writeVarBytes]. + /// + /// Example: + /// ```dart + /// final data = reader.readVarBytes(); + /// print('Read ${data.length} bytes'); + /// ``` + /// + /// This is equivalent to: + /// ```dart + /// final length = reader.readVarUint(); + /// final data = reader.readBytes(length); + /// ``` + /// + /// **Performance:** Zero-copy operation using buffer views. + /// + /// Asserts bounds in debug mode if insufficient bytes are available. + @pragma('vm:prefer-inline') + Uint8List readVarBytes() { + final length = readVarUint(); + return readBytes(length); + } + /// Reads a UTF-8 encoded string of the specified byte length. /// /// [length] is the number of UTF-8 bytes to read (not the number of - /// characters). - /// The string is decoded directly from the buffer without copying. + /// characters). The string is decoded directly from the buffer without + /// copying. /// /// [allowMalformed] controls how invalid UTF-8 sequences are handled: /// - If true: replaces malformed sequences with U+FFFD (�) /// - If false (default): throws [FormatException] on invalid UTF-8 /// - /// Note: This reads a fixed number of bytes. For length-prefixed strings, - /// first read the byte length (e.g., with [readVarUint] or [readVarInt]), - /// then call this method. + /// **Common pattern:** Read length first, then string: + /// + /// ```dart + /// // Length-prefixed string + /// final byteLength = reader.readVarUint(); + /// final text = reader.readString(byteLength); + /// // Fixed-length magic string + /// final magic = reader.readString(4); // e.g., "PNG\n" + /// ``` + /// + /// **Performance:** Zero-copy operation using buffer views. @pragma('vm:prefer-inline') String readString(int length, {bool allowMalformed = false}) { if (length == 0) { @@ -270,6 +404,30 @@ extension type const BinaryReader._(_ReaderState _ctx) { return utf8.decode(view, allowMalformed: allowMalformed); } + /// Reads a length-prefixed UTF-8 encoded string. + /// + /// First reads the UTF-8 byte length as a VarUint, then reads and decodes + /// the UTF-8 string data. + /// + /// [allowMalformed] controls how invalid UTF-8 sequences are handled: + /// - If true: replaces invalid sequences with U+FFFD (�) + /// - If false (default): throws [FormatException] on malformed UTF-8 + /// + /// This is the counterpart to [BinaryWriter.writeVarString]. + /// + /// Example: + /// ```dart + /// final text = reader.readVarString(); + /// print(text); // 'Hello, 世界! 🌍' + /// ``` + /// + /// Throws [AssertionError] if attempting to read past buffer end. + @pragma('vm:prefer-inline') + String readVarString({bool allowMalformed = false}) { + final length = readVarUint(); + return readString(length, allowMalformed: allowMalformed); + } + /// Reads bytes without advancing the read position. /// /// This allows inspecting upcoming data without consuming it. @@ -280,6 +438,16 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// /// Returns a view of the buffer without copying data. /// Asserts bounds in debug mode if peeking past buffer end. + /// + /// Example: + /// ```dart + /// // Check message type without consuming the byte + /// final typeBytes = reader.peekBytes(1); + /// if (typeBytes[0] == 0x42) { + /// // Handle type 0x42 + /// } + /// final actualType = reader.readUint8(); // Now read it + /// ``` @pragma('vm:prefer-inline') Uint8List peekBytes(int length, [int? offset]) { assert(length >= 0, 'Length must be non-negative'); @@ -302,6 +470,15 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// More efficient than reading and discarding data. /// /// Asserts bounds in debug mode if skipping past buffer end. + /// + /// Example: + /// ```dart + /// // Skip optional padding or reserved fields + /// reader.skip(4); // Skip 4 bytes of padding + /// // Skip unknown message payload + /// final payloadSize = reader.readUint32(); + /// reader.skip(payloadSize); + /// ``` void skip(int length) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Skip'); diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 734793a..dd385a6 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,5 +1,8 @@ import 'dart:typed_data'; +import '../pro_binary.dart' show BinaryReader; +import 'binary_reader.dart' show BinaryReader; + /// A high-performance binary writer for encoding data into a byte buffer. /// /// Provides methods for writing various data types including: @@ -14,9 +17,18 @@ import 'dart:typed_data'; /// Example: /// ```dart /// final writer = BinaryWriter(); +/// +/// // Write various data types /// writer.writeUint32(42); -/// writer.writeString('Hello'); -/// final bytes = writer.takeBytes(); +/// writer.writeFloat64(3.14); +/// // Write length-prefixed string +/// final text = 'Hello, World!'; +/// final utf8Bytes = utf8.encode(text); +/// writer.writeVarUint(utf8Bytes.length); +/// writer.writeString(text); +/// // Extract bytes and optionally reuse writer +/// final bytes = writer.takeBytes(); // Resets writer for reuse +/// // or: final bytes = writer.toBytes(); // Keeps writer state /// ``` extension type BinaryWriter._(_WriterState _ctx) { /// Creates a new [BinaryWriter] with the specified initial buffer size. @@ -38,8 +50,22 @@ extension type BinaryWriter._(_WriterState _ctx) { /// highest bit as a continuation flag. This is more space-efficient for /// small unsigned numbers (1-5 bytes for typical 32-bit values). /// - /// Only non-negative integers are supported. For signed integers with - /// efficient negative number encoding, use [writeVarInt] instead. + /// **When to use:** + /// - Counts, lengths, array sizes (always non-negative) + /// - IDs, indices, and other naturally unsigned values + /// - When values are typically small (< 128 uses only 1 byte) + /// + /// **Performance:** Values 0-127 use fast single-byte path. + /// + /// For signed integers that may be negative, use [writeVarInt] instead, + /// which uses ZigZag encoding to efficiently handle negative values. + /// + /// Example: + /// ```dart + /// writer.writeVarUint(42); // 1 byte + /// writer.writeVarUint(300); // 2 bytes + /// writer.writeVarUint(1000000); // 3 bytes + /// ``` @pragma('vm:prefer-inline') void writeVarUint(int value) { // Fast path for single-byte VarInt @@ -72,6 +98,21 @@ extension type BinaryWriter._(_WriterState _ctx) { /// /// The encoded value is then written using VarInt format. This is more /// efficient than [writeVarUint] for signed values that may be negative. + /// + /// **When to use:** + /// - Signed values where negatives are common (deltas, offsets) + /// - Values centered around zero + /// - Temperature readings, coordinate deltas, etc. + /// + /// **Performance:** Small absolute values (both + and -) encode efficiently. + /// + /// Example: + /// ```dart + /// writer.writeVarInt(0); // 1 byte + /// writer.writeVarInt(-1); // 1 byte + /// writer.writeVarInt(42); // 1 byte + /// writer.writeVarInt(-42); // 1 byte + /// ``` void writeVarInt(int value) { // ZigZag: (n << 1) ^ (n >> 63) // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 @@ -81,6 +122,11 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes an 8-bit unsigned integer (0-255). /// + /// Example: + /// ```dart + /// writer.writeUint8(0x42); // Write message type + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint8(int value) { @@ -92,6 +138,11 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes an 8-bit signed integer (-128 to 127). /// + /// Example: + /// ```dart + /// writer.writeInt8(-50); // Write temperature offset + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt8(int value) { @@ -104,6 +155,12 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 16-bit unsigned integer (0-65535). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeUint16(8080); // Port number + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint16(int value, [Endian endian = .big]) { @@ -117,6 +174,12 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 16-bit signed integer (-32768 to 32767). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeInt16(-100); // Temperature in Celsius + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt16(int value, [Endian endian = .big]) { @@ -130,6 +193,12 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 32-bit unsigned integer (0 to 4,294,967,295). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeUint32(1640995200); // Unix timestamp + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint32(int value, [Endian endian = .big]) { @@ -143,6 +212,12 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeInt32(-500000); // Account balance in cents + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt32(int value, [Endian endian = .big]) { @@ -156,7 +231,14 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 64-bit unsigned integer (0 to 9,223,372,036,854,775,807). /// /// Note: Dart's integer precision is limited to 2^53 for web targets. + /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeUint64(9007199254740991); // Max safe JS int + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeUint64(int value, [Endian endian = .big]) { @@ -170,7 +252,14 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 64-bit signed integer. /// /// Note: Dart's integer precision is limited to 2^53 for web targets. + /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeInt64(1234567890123456); // Large ID + /// ``` + /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') void writeInt64(int value, [Endian endian = .big]) { @@ -184,6 +273,11 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 32-bit floating-point number (IEEE 754 single precision). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeFloat32(3.14); // Pi approximation + /// ``` @pragma('vm:prefer-inline') void writeFloat32(double value, [Endian endian = .big]) { _ctx.ensureFourBytes(); @@ -194,6 +288,11 @@ extension type BinaryWriter._(_WriterState _ctx) { /// Writes a 64-bit floating-point number (IEEE 754 double precision). /// /// [endian] specifies byte order (defaults to big-endian). + /// + /// Example: + /// ```dart + /// writer.writeFloat64(3.14159265359); // High-precision pi + /// ``` @pragma('vm:prefer-inline') void writeFloat64(double value, [Endian endian = .big]) { _ctx.ensureEightBytes(); @@ -205,6 +304,14 @@ extension type BinaryWriter._(_WriterState _ctx) { /// /// [offset] specifies the starting position in [bytes] (defaults to 0). /// [length] specifies how many bytes to write (defaults to remaining bytes). + /// + /// Example: + /// ```dart + /// final data = [1, 2, 3, 4, 5]; + /// writer.writeBytes(data); // Write all 5 bytes + /// writer.writeBytes(data, 2); // Write [3, 4, 5] + /// writer.writeBytes(data, 1, 3); // Write [2, 3, 4] + /// ``` @pragma('vm:prefer-inline') void writeBytes(List bytes, [int offset = 0, int? length]) { final len = length ?? (bytes.length - offset); @@ -214,6 +321,31 @@ extension type BinaryWriter._(_WriterState _ctx) { _ctx.offset += len; } + /// Writes a length-prefixed byte array. + /// + /// First writes the length as a VarUint, followed by the byte data. + /// This is useful for serializing binary blobs of unknown size. + /// + /// This is the counterpart to [BinaryReader.readVarBytes]. + /// + /// Example: + /// ```dart + /// final imageData = [/* ... binary data ... */]; + /// writer.writeVarBytes(imageData); + /// // Length is automatically written as VarUint + /// ``` + /// + /// This is equivalent to: + /// ```dart + /// writer.writeVarUint(bytes.length); + /// writer.writeBytes(bytes); + /// ``` + @pragma('vm:prefer-inline') + void writeVarBytes(List bytes) { + writeVarUint(bytes.length); + writeBytes(bytes); + } + /// Writes a UTF-8 encoded string. /// /// The string is encoded directly to UTF-8 bytes with optimized handling for: @@ -225,9 +357,21 @@ extension type BinaryWriter._(_WriterState _ctx) { /// - If true (default): replaces lone surrogates with U+FFFD (�) /// - If false: throws [FormatException] on malformed input /// - /// Note: This does NOT write the string length. For length-prefixed strings, - /// first call [writeVarUint] or [writeVarInt] with the byte length, then - /// call this method. + /// **Important:** This does NOT write the string length. For self-describing + /// data, write the length first: + /// + /// Example: + /// ```dart + /// // Length-prefixed string (recommended for most protocols) + /// final text = 'Hello, 世界! 🌍'; + /// final utf8Bytes = utf8.encode(text); + /// writer.writeVarUint(utf8Bytes.length); // Write byte length + /// writer.writeString(text); // Write string data + /// // Or for simple fixed-length strings: + /// writer.writeString('MAGIC'); // No length prefix needed + /// ``` + /// + /// **Performance:** Highly optimized for ASCII-heavy strings. @pragma('vm:prefer-inline') void writeString(String value, {bool allowMalformed = true}) { final len = value.length; @@ -338,12 +482,50 @@ extension type BinaryWriter._(_WriterState _ctx) { _ctx.offset = offset; } + /// Writes a length-prefixed UTF-8 encoded string. + /// + /// First writes the UTF-8 byte length as a VarUint, followed by the + /// UTF-8 encoded string data. + /// + /// [allowMalformed] controls how invalid UTF-16 sequences are handled: + /// - If true (default): replaces lone surrogates with U+FFFD (�) + /// - If false: throws [FormatException] on malformed input + /// + /// Example: + /// ```dart + /// final text = 'Hello, 世界! 🌍'; + /// writer.writeVarString(text); + /// ``` + /// This is equivalent to: + /// ```dart + /// final utf8Bytes = utf8.encode(text); + /// writer.writeVarUint(utf8Bytes.length); + /// writer.writeString(text); + /// ``` + @pragma('vm:prefer-inline') + void writeVarString(String value, {bool allowMalformed = true}) { + final utf8Length = getUtf8Length(value); + writeVarUint(utf8Length); + writeString(value, allowMalformed: allowMalformed); + } + /// Extracts all written bytes and resets the writer. /// /// After calling this method, the writer is reset and ready for reuse. /// This is more efficient than creating a new writer for each operation. /// /// Returns a view of the written bytes (no copying occurs). + /// + /// **Use case:** When you're done with this batch and want to start fresh. + /// + /// Example: + /// ```dart + /// final writer = BinaryWriter(); + /// writer.writeUint32(42); + /// final packet1 = writer.takeBytes(); // Get bytes and reset + /// writer.writeUint32(100); // Writer is ready for reuse + /// final packet2 = writer.takeBytes(); + /// ``` @pragma('vm:prefer-inline') Uint8List takeBytes() { final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); @@ -355,6 +537,17 @@ extension type BinaryWriter._(_WriterState _ctx) { /// /// Unlike [takeBytes], this does not reset the writer's state. /// Subsequent writes will continue appending to the buffer. + /// + /// **Use case:** When you need to inspect or copy data mid-stream. + /// + /// Example: + /// ```dart + /// final writer = BinaryWriter(); + /// writer.writeUint32(42); + /// final snapshot = writer.toBytes(); // Peek at current data + /// writer.writeUint32(100); // Continue writing + /// final final = writer.takeBytes(); // Get all data + /// ``` @pragma('vm:prefer-inline') Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); @@ -489,3 +682,101 @@ final class _WriterState { capacity = newCapacity; } } + +/// 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 8 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: ~8 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 s) { + if (s.isEmpty) { + return 0; + } + + final len = s.length; + var bytes = 0; + var i = 0; + + while (i < len) { + final c = s.codeUnitAt(i); + + // ASCII fast path + if (c < 0x80) { + // Process 8 ASCII characters at a time + final end = len - 8; + while (i <= end) { + final mask = + s.codeUnitAt(i) | + s.codeUnitAt(i + 1) | + s.codeUnitAt(i + 2) | + s.codeUnitAt(i + 3) | + s.codeUnitAt(i + 4) | + s.codeUnitAt(i + 5) | + s.codeUnitAt(i + 6) | + s.codeUnitAt(i + 7); + + if (mask >= 0x80) { + break; + } + + i += 8; + bytes += 8; + } + + // Handle remaining ASCII characters + while (i < len && s.codeUnitAt(i) < 0x80) { + i++; + bytes++; + } + if (i >= len) { + return bytes; + } + continue; + } + + // 2-byte sequence + if (c < 0x800) { + bytes += 2; + i++; + } + // 3-byte sequence + else if (c >= 0xD800 && c <= 0xDBFF && i + 1 < len) { + final next = s.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; +} diff --git a/pubspec.yaml b/pubspec.yaml index c8b216a..dd29268 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: 2.2.0 +version: 3.0.0 repository: https://github.com/pro100andrey/pro_binary issue_tracker: https://github.com/pro100andrey/pro_binary/issues diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart index 23d2097..414b142 100644 --- a/test/binary_reader_performance_test.dart +++ b/test/binary_reader_performance_test.dart @@ -1,6 +1,31 @@ +import 'dart:convert'; + import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; +const string = 'Hello, World!'; +const longString = + 'The quick brown fox 🦊 jumps over the lazy dog 🐕. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit 🔬. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua 🏋️. ' + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + 'ut aliquip ex ea commodo consequat ☕. ' + 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' + 'dolore eu fugiat nulla pariatur 🌈. ' + 'Excepteur sint occaecat cupidatat non proident, ' + 'sunt in culpa qui officia deserunt mollit anim id est laborum. 🎯 ' + '🚀 TEST EXTENSION: Adding a second long paragraph to truly stress the ' + 'UTF-8 encoding logic. This includes more complex characters like the ' + 'Zodiac signs ♒️ ♓️ ♈️ ♉️ and some CJK characters like 日本語. ' + 'We also add a few more standard 4-byte emoji like a stack of money 💰, ' + 'a ghost 👻, and a classic thumbs up 👍 to ensure maximum complexity ' + 'in the string encoding process. The purpose of this extra length is to ' + 'force the `_ensureSize` method to be called multiple times and ensure ' + 'that the buffer resizing and copying overhead is measured correctly. ' + 'This paragraph is deliberately longer to ensure that the total byte ' + 'count for UTF-8 is significantly larger than the initial string length. ' + '🏁'; + class BinaryReaderBenchmark extends BenchmarkBase { BinaryReaderBenchmark() : super('BinaryReader performance test'); @@ -8,11 +33,6 @@ class BinaryReaderBenchmark extends BenchmarkBase { @override void setup() { - const string = 'Hello, World!'; - const longString = - 'Some more data to increase buffer usage. ' - 'The quick brown fox jumps over the lazy dog.'; - final writer = BinaryWriter() ..writeUint8(42) ..writeInt8(-42) @@ -25,10 +45,8 @@ class BinaryReaderBenchmark extends BenchmarkBase { ..writeFloat32(3.14, .little) ..writeFloat64(3.141592653589793, .little) ..writeFloat64(2.718281828459045) - ..writeInt8(string.length) - ..writeString(string) - ..writeInt32(longString.length) - ..writeString(longString) + ..writeVarString(string) + ..writeVarString(longString) ..writeBytes([]) ..writeBytes(List.filled(120, 100)); @@ -53,10 +71,8 @@ class BinaryReaderBenchmark extends BenchmarkBase { final _ = reader.readFloat32(.little); final _ = reader.readFloat64(.little); final _ = reader.readFloat64(.little); - final length = reader.readInt8(); - final _ = reader.readString(length); - final longLength = reader.readInt32(); - final _ = reader.readString(longLength); + final _ = reader.readVarString(); + final _ = reader.readVarString(); final _ = reader.readBytes(0); final _ = reader.readBytes(120); @@ -70,6 +86,71 @@ class BinaryReaderBenchmark extends BenchmarkBase { } } +class GetStringLengthBenchmark extends BenchmarkBase { + GetStringLengthBenchmark() : super('GetStringLength performance test'); + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + final _ = getUtf8Length(string); + final _ = getUtf8Length(longString); + } + } + + static void main() { + GetStringLengthBenchmark().report(); + } +} + +class GetStringLengthUtf8Benchmark extends BenchmarkBase { + GetStringLengthUtf8Benchmark() + : super('GetStringLengthUtf8 performance test'); + + @override + void exercise() => run(); + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + final _ = utf8.encode(string).length; + final _ = utf8.encode(longString).length; + } + } + + static void main() { + GetStringLengthUtf8Benchmark().report(); + } +} + void main() { BinaryReaderBenchmark.main(); + GetStringLengthBenchmark.main(); + GetStringLengthUtf8Benchmark.main(); } diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 1a307a2..29e8c0b 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -1243,6 +1243,148 @@ void main() { expect(reader2.readBytes(3), equals([51, 52, 53])); }); + test('readVarBytes basic usage', () { + final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarBytes(), equals([1, 2, 3, 4])); + }); + + test('readVarBytes with empty array', () { + final writer = BinaryWriter()..writeVarBytes([]); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarBytes(), equals([])); + }); + + test('readVarBytes multiple arrays', () { + final writer = BinaryWriter() + ..writeVarBytes([10, 20]) + ..writeVarBytes([30, 40, 50]) + ..writeVarBytes([60]); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarBytes(), equals([10, 20])); + expect(reader.readVarBytes(), equals([30, 40, 50])); + expect(reader.readVarBytes(), equals([60])); + }); + + test('readVarBytes with large array', () { + final writer = BinaryWriter(); + final data = List.generate(500, (i) => (i * 3) & 0xFF); + writer.writeVarBytes(data); + final reader = BinaryReader(writer.takeBytes()); + + final result = reader.readVarBytes(); + expect(result, equals(data)); + expect(result.length, equals(500)); + }); + + test('readVarBytes throws on truncated length', () { + final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint + final reader = BinaryReader(bytes); + + expect( + reader.readVarBytes, + throwsA(isA()), + ); + }); + + test('readVarBytes throws when not enough data', () { + final bytes = Uint8List.fromList([5, 1, 2]); // Length=5, only 2 bytes + final reader = BinaryReader(bytes); + + expect( + reader.readVarBytes, + throwsA(isA()), + ); + }); + + test('readVarBytes preserves binary data', () { + final writer = BinaryWriter(); + // Test with all byte values 0-255 + final allBytes = List.generate(256, (i) => i); + writer.writeVarBytes(allBytes); + + final reader = BinaryReader(writer.takeBytes()); + final result = reader.readVarBytes(); + + expect(result, equals(allBytes)); + for (var i = 0; i < 256; i++) { + expect(result[i], equals(i), reason: 'Byte $i mismatch'); + } + }); + + test('readVarString basic usage', () { + final writer = BinaryWriter()..writeVarString('Hello'); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarString(), equals('Hello')); + }); + + test('readVarString with UTF-8 multi-byte', () { + final writer = BinaryWriter()..writeVarString('世界'); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarString(), equals('世界')); + }); + + test('readVarString with emoji', () { + final writer = BinaryWriter()..writeVarString('🌍🎉'); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarString(), equals('🌍🎉')); + }); + + test('readVarString with empty string', () { + final writer = BinaryWriter()..writeVarString(''); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarString(), equals('')); + }); + + test('readVarString multiple strings', () { + final writer = BinaryWriter() + ..writeVarString('First') + ..writeVarString('Second 测试') + ..writeVarString('Third 🎉'); + final reader = BinaryReader(writer.takeBytes()); + + expect(reader.readVarString(), equals('First')); + expect(reader.readVarString(), equals('Second 测试')); + expect(reader.readVarString(), equals('Third 🎉')); + }); + + test('readVarString with allowMalformed=false on valid data', () { + final writer = BinaryWriter()..writeVarString('Valid UTF-8'); + final reader = BinaryReader(writer.takeBytes()); + + expect( + reader.readVarString, + returnsNormally, + ); + }); + + test('readVarString throws on truncated length', () { + final bytes = Uint8List.fromList([0x85]); // Incomplete VarUint + final reader = BinaryReader(bytes); + + expect( + reader.readVarString, + throwsA(isA()), + ); + }); + + test('readVarString throws when not enough data for string', () { + final bytes = Uint8List.fromList([5, 65, 66]); // Length=5, only 2 bytes + final reader = BinaryReader(bytes); + + expect( + reader.readVarString, + throwsA(isA()), + ); + }); + test('baseOffset with readString containing multi-byte UTF-8', () { const text = 'Привет мир! 🌍'; final encoded = utf8.encode(text); @@ -1261,5 +1403,54 @@ void main() { expect(result, equals(text)); }); }); + + group('Getter properties', () { + test('offset getter returns current read position', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeUint16(2) + ..writeUint32(3); + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.offset, equals(0)); + reader.readUint8(); + expect(reader.offset, equals(1)); + reader.readUint16(); + expect(reader.offset, equals(3)); + reader.readUint32(); + expect(reader.offset, equals(7)); + }); + + test('length getter returns total buffer length', () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(bytes); + + expect(reader.length, equals(5)); + reader.readUint8(); + expect(reader.length, equals(5)); // Length doesn't change + reader.readUint32(); + expect(reader.length, equals(5)); + }); + + test('offset and length used together to calculate availableBytes', () { + final bytes = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(bytes); + + expect(reader.length, equals(8)); + expect(reader.offset, equals(0)); + expect(reader.availableBytes, equals(8)); + + reader.readUint32(); + expect(reader.offset, equals(4)); + expect(reader.length, equals(8)); + expect(reader.availableBytes, equals(4)); + + reader.readUint32(); + expect(reader.offset, equals(8)); + expect(reader.length, equals(8)); + expect(reader.availableBytes, equals(0)); + }); + }); }); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 2fbd391..cb97182 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:typed_data'; import 'package:pro_binary/pro_binary.dart'; @@ -1363,6 +1364,232 @@ void main() { }); }); + group('VarBytes operations', () { + test('writeVarBytes with empty array', () { + final writer = BinaryWriter()..writeVarBytes([]); + final bytes = writer.takeBytes(); + + expect(bytes, equals([0])); // Just length 0 + }); + + test('writeVarBytes with small array', () { + final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4]); + final bytes = writer.takeBytes(); + + expect(bytes[0], equals(4)); // VarUint length + expect(bytes.sublist(1), equals([1, 2, 3, 4])); + }); + + test('writeVarBytes with 127 bytes (single-byte VarUint)', () { + final writer = BinaryWriter(); + final data = List.generate(127, (i) => i); + writer.writeVarBytes(data); + final bytes = writer.takeBytes(); + + expect(bytes[0], equals(127)); // Single-byte VarUint + expect(bytes.length, equals(128)); // 1 (length) + 127 (data) + }); + + test('writeVarBytes with 128 bytes (two-byte VarUint)', () { + final writer = BinaryWriter(); + final data = List.generate(128, (i) => i & 0xFF); + writer.writeVarBytes(data); + final bytes = writer.takeBytes(); + + expect(bytes[0], equals(0x80)); // First byte of VarUint 128 + expect(bytes[1], equals(0x01)); // Second byte of VarUint 128 + expect(bytes.length, equals(130)); // 2 (length) + 128 (data) + }); + + test('writeVarBytes with large array', () { + final writer = BinaryWriter(); + final data = List.generate(1000, (i) => (i * 7) & 0xFF); + writer.writeVarBytes(data); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final length = reader.readVarUint(); + expect(length, equals(1000)); + + final readData = reader.readBytes(1000); + expect(readData, equals(data)); + }); + + test('writeVarBytes multiple arrays', () { + final writer = BinaryWriter() + ..writeVarBytes([1, 2]) + ..writeVarBytes([3, 4, 5]) + ..writeVarBytes([6]); + + final reader = BinaryReader(writer.toBytes()); + expect(reader.readVarBytes(), equals([1, 2])); + expect(reader.readVarBytes(), equals([3, 4, 5])); + expect(reader.readVarBytes(), equals([6])); + }); + + test('writeVarBytes round-trip', () { + final writer = BinaryWriter(); + final original = List.generate(256, (i) => i); + writer.writeVarBytes(original); + + final reader = BinaryReader(writer.takeBytes()); + final result = reader.readVarBytes(); + + expect(result, equals(original)); + }); + }); + + group('VarString operations', () { + test('writeVarString with ASCII string', () { + final writer = BinaryWriter()..writeVarString('Hello'); + final bytes = writer.takeBytes(); + + expect(bytes[0], equals(5)); // VarUint length + expect(bytes.sublist(1), equals([72, 101, 108, 108, 111])); // 'Hello' + }); + + test('writeVarString with UTF-8 multi-byte characters', () { + final writer = BinaryWriter() + ..writeVarString('世界'); // 2 characters, 6 bytes in UTF-8 + final bytes = writer.takeBytes(); + + expect(bytes[0], equals(6)); // VarUint length (6 bytes) + expect(bytes.length, equals(7)); // 1 (length) + 6 (data) + }); + + test('writeVarString with emoji', () { + final writer = BinaryWriter() + ..writeVarString('🌍'); // 1 character, 4 bytes in UTF-8 + final bytes = writer.takeBytes(); + + expect(bytes[0], equals(4)); // VarUint length + expect(bytes.length, equals(5)); // 1 (length) + 4 (data) + }); + + test('writeVarString with empty string', () { + final writer = BinaryWriter()..writeVarString(''); + final bytes = writer.takeBytes(); + + expect(bytes, equals([0])); // Just length 0 + }); + + test('writeVarString with mixed content', () { + final writer = BinaryWriter()..writeVarString('Hi 世界 🌍!'); + final bytes = writer.takeBytes(); + + // 'Hi ' = 3, '世界' = 6, ' ' = 1, '🌍' = 4, '!' = 1 => 15 bytes + expect(bytes[0], equals(15)); // VarUint length + expect(bytes.length, equals(16)); // 1 + 15 + }); + + test('writeVarString round-trip with reader', () { + final writer = BinaryWriter(); + const testString = 'Test 测试 🎉'; + writer.writeVarString(testString); + + final reader = BinaryReader(writer.toBytes()); + final result = reader.readVarString(); + + expect(result, equals(testString)); + }); + + test('writeVarString with malformed handling', () { + final writer = BinaryWriter(); + // Lone high surrogate (U+D800) + final malformed = String.fromCharCode(0xD800); + + // Default allowMalformed=true should handle it + expect( + () => writer.writeVarString(malformed), + returnsNormally, + ); + }); + }); + + group('getUtf8Length function', () { + test('getUtf8Length with ASCII only', () { + expect(getUtf8Length('Hello'), equals(5)); + expect(getUtf8Length('ABCDEFGH'), equals(8)); // Fast path + }); + + test('getUtf8Length with empty string', () { + expect(getUtf8Length(''), equals(0)); + }); + + test('getUtf8Length with 2-byte UTF-8 chars', () { + expect(getUtf8Length('café'), equals(5)); // 'caf' = 3, 'é' = 2 + expect(getUtf8Length('Привет'), equals(12)); // Each Cyrillic = 2 bytes + }); + + test('getUtf8Length with 3-byte UTF-8 chars', () { + expect(getUtf8Length('世界'), equals(6)); // Each Chinese = 3 bytes + expect(getUtf8Length('你好'), equals(6)); + }); + + test('getUtf8Length with 4-byte UTF-8 chars (emoji)', () { + expect(getUtf8Length('🌍'), equals(4)); + expect(getUtf8Length('🎉'), equals(4)); + expect(getUtf8Length('😀'), equals(4)); + }); + + test('getUtf8Length with mixed content', () { + // 'Hello' = 5, ', ' = 2, '世界' = 6, '! ' = 2, '🌍' = 4 + expect(getUtf8Length('Hello, 世界! 🌍'), equals(19)); + }); + + test('getUtf8Length matches actual UTF-8 encoding', () { + final strings = [ + 'Test', + 'Тест', + '测试', + '🧪', + 'Mix テスト 123', + 'A' * 100, // Long ASCII for fast path + ]; + + for (final str in strings) { + final calculated = getUtf8Length(str); + final actual = utf8.encode(str).length; + expect( + calculated, + equals(actual), + reason: 'Failed for string: "$str"', + ); + } + }); + + test('getUtf8Length with surrogate pairs', () { + // Valid surrogate pair forms emoji + final emoji = String.fromCharCodes([0xD83C, 0xDF0D]); // 🌍 + expect(getUtf8Length(emoji), equals(4)); + }); + + test('getUtf8Length with malformed high surrogate', () { + // High surrogate (0xD800-0xDBFF) not followed by low surrogate + // This triggers the malformed surrogate pair path in getUtf8Length + final malformed = String.fromCharCodes([ + 0xD800, + 0x0041, + ]); // High surrogate + 'A' + expect( + getUtf8Length(malformed), + equals(4), + ); // 3 bytes (replacement) + 1 byte (A) + }); + + test('getUtf8Length with lone high surrogate at end', () { + // High surrogate at the end of string (also malformed) + final malformed = String.fromCharCodes([ + 0x0041, + 0xD800, + ]); // 'A' + high surrogate + expect( + getUtf8Length(malformed), + equals(4), + ); // 1 byte (A) + 3 bytes (replacement) + }); + }); + group('Special UTF-8 cases', () { test('writeString with only ASCII (fast path)', () { const str = 'OnlyASCII123'; From 8df2550315d36adf1650546a94d87c0166ea236e Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 25 Dec 2025 16:44:44 +0200 Subject: [PATCH 08/29] refactor: Rename test descriptions for clarity in BinaryReader and BinaryWriter tests --- test/binary_reader_test.dart | 22 +--- test/binary_writer_test.dart | 216 +++++++++++++++++------------------ 2 files changed, 109 insertions(+), 129 deletions(-) diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 29e8c0b..91acb2f 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -5,7 +5,7 @@ import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; void main() { - group('FastBinaryReader', () { + group('BinaryReader', () { test('readUint8', () { final buffer = Uint8List.fromList([0x01]); final reader = BinaryReader(buffer); @@ -422,17 +422,6 @@ void main() { expect(reader.availableBytes, equals(1)); }); - test('usedBytes returns correct number of used bytes', () { - final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); - final reader = BinaryReader(buffer); - - expect(reader.offset, equals(0)); - reader.readUint8(); - expect(reader.offset, equals(1)); - reader.readBytes(2); - expect(reader.offset, equals(3)); - }); - test( 'peekBytes returns correct bytes without changing the internal state', () { @@ -564,15 +553,6 @@ void main() { expect(reader.readFloat64(), equals(2.0)); }); - test('readString with UTF-8 multi-byte characters', () { - const str = 'こんにちは世界'; // "Hello, World" in Japanese - final encoded = utf8.encode(str); - final buffer = Uint8List.fromList(encoded); - final reader = BinaryReader(buffer); - - expect(reader.readString(encoded.length), equals(str)); - }); - group('Boundary checks', () { test('readUint8 throws when buffer is empty', () { final buffer = Uint8List.fromList([]); diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index cb97182..73ac711 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -12,188 +12,188 @@ void main() { writer = BinaryWriter(); }); - test('should return empty list when takeBytes called on empty writer', () { + test('return empty list when takeBytes called on empty writer', () { expect(writer.takeBytes(), isEmpty); }); - test('should write single Uint8 value correctly', () { + test('write single Uint8 value correctly', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); }); - test('should write negative Int8 value correctly', () { + test('write negative Int8 value correctly', () { writer.writeInt8(-1); expect(writer.takeBytes(), [255]); }); - test('should write Uint16 in big-endian format', () { + test('write Uint16 in big-endian format', () { writer.writeUint16(256); expect(writer.takeBytes(), [1, 0]); }); - test('should write Uint16 in little-endian format', () { + test('write Uint16 in little-endian format', () { writer.writeUint16(256, .little); expect(writer.takeBytes(), [0, 1]); }); - test('should write Int16 in big-endian format', () { + test('write Int16 in big-endian format', () { writer.writeInt16(-1); expect(writer.takeBytes(), [255, 255]); }); - test('should write Int16 in little-endian format', () { + test('write Int16 in little-endian format', () { writer.writeInt16(-32768, .little); expect(writer.takeBytes(), [0, 128]); }); - test('should write Uint32 in big-endian format', () { + test('write Uint32 in big-endian format', () { writer.writeUint32(65536); expect(writer.takeBytes(), [0, 1, 0, 0]); }); - test('should write Uint32 in little-endian format', () { + test('write Uint32 in little-endian format', () { writer.writeUint32(65536, .little); expect(writer.takeBytes(), [0, 0, 1, 0]); }); - test('should write Int32 in big-endian format', () { + test('write Int32 in big-endian format', () { writer.writeInt32(-1); expect(writer.takeBytes(), [255, 255, 255, 255]); }); - test('should write Int32 in little-endian format', () { + test('write Int32 in little-endian format', () { writer.writeInt32(-2147483648, .little); expect(writer.takeBytes(), [0, 0, 0, 128]); }); - test('should write Uint64 in big-endian format', () { + test('write Uint64 in big-endian format', () { writer.writeUint64(4294967296); expect(writer.takeBytes(), [0, 0, 0, 1, 0, 0, 0, 0]); }); - test('should write Uint64 in little-endian format', () { + test('write Uint64 in little-endian format', () { writer.writeUint64(4294967296, .little); expect(writer.takeBytes(), [0, 0, 0, 0, 1, 0, 0, 0]); }); - test('should write Int64 in big-endian format', () { + test('write Int64 in big-endian format', () { writer.writeInt64(-1); expect(writer.takeBytes(), [255, 255, 255, 255, 255, 255, 255, 255]); }); - test('should write Int64 in little-endian format', () { + test('write Int64 in little-endian format', () { writer.writeInt64(-9223372036854775808, .little); expect(writer.takeBytes(), [0, 0, 0, 0, 0, 0, 0, 128]); }); - test('should write Float32 in big-endian format', () { + test('write Float32 in big-endian format', () { writer.writeFloat32(3.1415927); expect(writer.takeBytes(), [64, 73, 15, 219]); }); - test('should write Float32 in little-endian format', () { + test('write Float32 in little-endian format', () { writer.writeFloat32(3.1415927, .little); expect(writer.takeBytes(), [219, 15, 73, 64]); }); - test('should write Float64 in big-endian format', () { + test('write Float64 in big-endian format', () { writer.writeFloat64(3.141592653589793); expect(writer.takeBytes(), [64, 9, 33, 251, 84, 68, 45, 24]); }); - test('should write Float64 in little-endian format', () { + test('write Float64 in little-endian format', () { writer.writeFloat64(3.141592653589793, .little); expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); }); - test('should write VarInt single byte (0)', () { + test('write VarInt single byte (0)', () { writer.writeVarUint(0); expect(writer.takeBytes(), [0]); }); - test('should write VarInt single byte (127)', () { + test('write VarInt single byte (127)', () { writer.writeVarUint(127); expect(writer.takeBytes(), [127]); }); - test('should write VarInt two bytes (128)', () { + test('write VarInt two bytes (128)', () { writer.writeVarUint(128); expect(writer.takeBytes(), [0x80, 0x01]); }); - test('should write VarInt two bytes (300)', () { + test('write VarInt two bytes (300)', () { writer.writeVarUint(300); expect(writer.takeBytes(), [0xAC, 0x02]); }); - test('should write VarInt three bytes (16384)', () { + test('write VarInt three bytes (16384)', () { writer.writeVarUint(16384); expect(writer.takeBytes(), [0x80, 0x80, 0x01]); }); - test('should write VarInt four bytes (2097151)', () { + test('write VarInt four bytes (2097151)', () { writer.writeVarUint(2097151); expect(writer.takeBytes(), [0xFF, 0xFF, 0x7F]); }); - test('should write VarInt five bytes (268435455)', () { + test('write VarInt five bytes (268435455)', () { writer.writeVarUint(268435455); expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0x7F]); }); - test('should write VarInt large value', () { + test('write VarInt large value', () { writer.writeVarUint(1 << 30); expect(writer.takeBytes(), [0x80, 0x80, 0x80, 0x80, 0x04]); }); - test('should write ZigZag encoding for positive values', () { + test('write ZigZag encoding for positive values', () { writer.writeVarInt(0); expect(writer.takeBytes(), [0]); }); - test('should write ZigZag encoding for positive value 1', () { + test('write ZigZag encoding for positive value 1', () { writer.writeVarInt(1); expect(writer.takeBytes(), [2]); }); - test('should write ZigZag encoding for negative value -1', () { + test('write ZigZag encoding for negative value -1', () { writer.writeVarInt(-1); expect(writer.takeBytes(), [1]); }); - test('should write ZigZag encoding for positive value 2', () { + test('write ZigZag encoding for positive value 2', () { writer.writeVarInt(2); expect(writer.takeBytes(), [4]); }); - test('should write ZigZag encoding for negative value -2', () { + test('write ZigZag encoding for negative value -2', () { writer.writeVarInt(-2); expect(writer.takeBytes(), [3]); }); - test('should write ZigZag encoding for large positive value', () { + test('write ZigZag encoding for large positive value', () { writer.writeVarInt(2147483647); expect(writer.takeBytes(), [0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); }); - test('should write ZigZag encoding for large negative value', () { + test('write ZigZag encoding for large negative value', () { writer.writeVarInt(-2147483648); expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); }); - test('should write byte array correctly', () { + test('write byte array correctly', () { writer.writeBytes([1, 2, 3, 4, 5]); expect(writer.takeBytes(), [1, 2, 3, 4, 5]); }); - test('should encode string to UTF-8 bytes correctly', () { + test('encode string to UTF-8 bytes correctly', () { writer.writeString('Hello, World!'); expect(writer.takeBytes(), [ 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33, // ASCII ]); }); - test('should handle complex sequence of different data types', () { + test('handle complex sequence of different data types', () { final writer = BinaryWriter() ..writeUint8(42) ..writeInt8(-42) @@ -243,7 +243,7 @@ void main() { }, ); - test('should allow reusing writer after takeBytes', () { + test('allow reusing writer after takeBytes', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); @@ -251,7 +251,7 @@ void main() { expect(writer.takeBytes(), [2]); }); - test('should handle writing large data sets efficiently', () { + test('handle writing large data sets efficiently', () { final largeData = Uint8List.fromList( List.generate(10000, (i) => i % 256), ); @@ -264,7 +264,7 @@ void main() { expect(result, equals(largeData)); }); - test('should track bytesWritten correctly', () { + test('track bytesWritten correctly', () { writer.writeUint8(1); expect(writer.bytesWritten, equals(1)); @@ -283,7 +283,7 @@ void main() { }); group('Input validation', () { - test('should throw AssertionError when Uint8 value is negative', () { + test('throw AssertionError when Uint8 value is negative', () { expect( () => writer.writeUint8(-1), throwsA( @@ -295,7 +295,7 @@ void main() { ); }); - test('should throw AssertionError when Uint8 value exceeds 255', () { + test('throw AssertionError when Uint8 value exceeds 255', () { expect( () => writer.writeUint8(256), throwsA( @@ -307,7 +307,7 @@ void main() { ); }); - test('should throw AssertionError when Int8 value is less than -128', () { + test('throw AssertionError when Int8 value is less than -128', () { expect( () => writer.writeInt8(-129), throwsA( @@ -319,7 +319,7 @@ void main() { ); }); - test('should throw AssertionError when Int8 value exceeds 127', () { + test('throw AssertionError when Int8 value exceeds 127', () { expect( () => writer.writeInt8(128), throwsA( @@ -331,7 +331,7 @@ void main() { ); }); - test('should throw AssertionError when Uint16 value is negative', () { + test('throw AssertionError when Uint16 value is negative', () { expect( () => writer.writeUint16(-1), throwsA( @@ -343,7 +343,7 @@ void main() { ); }); - test('should throw AssertionError when Uint16 value exceeds 65535', () { + test('throw AssertionError when Uint16 value exceeds 65535', () { expect( () => writer.writeUint16(65536), throwsA( @@ -370,7 +370,7 @@ void main() { }, ); - test('should throw AssertionError when Int16 value exceeds 32767', () { + test('throw AssertionError when Int16 value exceeds 32767', () { expect( () => writer.writeInt16(32768), throwsA( @@ -382,7 +382,7 @@ void main() { ); }); - test('should throw AssertionError when Uint32 value is negative', () { + test('throw AssertionError when Uint32 value is negative', () { expect( () => writer.writeUint32(-1), throwsA( @@ -441,7 +441,7 @@ void main() { }); group('toBytes', () { - test('should return current buffer without resetting writer state', () { + test('return current buffer without resetting writer state', () { writer ..writeUint8(42) ..writeUint8(100); @@ -476,14 +476,14 @@ void main() { }, ); - test('should return empty list when called on empty writer', () { + test('return empty list when called on empty writer', () { final bytes = writer.toBytes(); expect(bytes, isEmpty); }); }); group('clear', () { - test('should reset writer state without returning bytes', () { + test('reset writer state without returning bytes', () { writer ..writeUint8(42) ..writeUint8(100) @@ -493,7 +493,7 @@ void main() { expect(writer.toBytes(), isEmpty); }); - test('should allow writing new data after reset', () { + test('allow writing new data after reset', () { writer ..writeUint8(42) ..reset() @@ -502,26 +502,26 @@ void main() { expect(writer.toBytes(), equals([100])); }); - test('should be safe to call on empty writer', () { + test('be safe to call on empty writer', () { writer.reset(); expect(writer.bytesWritten, equals(0)); }); }); group('Edge cases', () { - test('should handle empty string correctly', () { + test('handle empty string correctly', () { writer.writeString(''); expect(writer.bytesWritten, equals(0)); expect(writer.toBytes(), isEmpty); }); - test('should handle empty byte array correctly', () { + test('handle empty byte array correctly', () { writer.writeBytes([]); expect(writer.bytesWritten, equals(0)); expect(writer.toBytes(), isEmpty); }); - test('should encode emoji characters correctly', () { + test('encode emoji characters correctly', () { const str = '🚀👨‍👩‍👧‍👦'; writer.writeString(str); final bytes = writer.takeBytes(); @@ -530,7 +530,7 @@ void main() { expect(reader.readString(bytes.length), equals(str)); }); - test('should handle Float32 NaN value correctly', () { + test('handle Float32 NaN value correctly', () { writer.writeFloat32(double.nan); final bytes = writer.takeBytes(); @@ -538,7 +538,7 @@ void main() { expect(reader.readFloat32().isNaN, isTrue); }); - test('should handle Float32 positive Infinity correctly', () { + test('handle Float32 positive Infinity correctly', () { writer.writeFloat32(double.infinity); final bytes = writer.takeBytes(); @@ -546,7 +546,7 @@ void main() { expect(reader.readFloat32(), equals(double.infinity)); }); - test('should handle Float32 negative Infinity correctly', () { + test('handle Float32 negative Infinity correctly', () { writer.writeFloat32(double.negativeInfinity); final bytes = writer.takeBytes(); @@ -554,7 +554,7 @@ void main() { expect(reader.readFloat32(), equals(double.negativeInfinity)); }); - test('should handle Float64 NaN value correctly', () { + test('handle Float64 NaN value correctly', () { writer.writeFloat64(double.nan); final bytes = writer.takeBytes(); @@ -562,7 +562,7 @@ void main() { expect(reader.readFloat64().isNaN, isTrue); }); - test('should handle Float64 positive Infinity correctly', () { + test('handle Float64 positive Infinity correctly', () { writer.writeFloat64(double.infinity); final bytes = writer.takeBytes(); @@ -570,7 +570,7 @@ void main() { expect(reader.readFloat64(), equals(double.infinity)); }); - test('should handle Float64 negative Infinity correctly', () { + test('handle Float64 negative Infinity correctly', () { writer.writeFloat64(double.negativeInfinity); final bytes = writer.takeBytes(); @@ -578,7 +578,7 @@ void main() { expect(reader.readFloat64(), equals(double.negativeInfinity)); }); - test('should preserve negative zero in Float64', () { + test('preserve negative zero in Float64', () { writer.writeFloat64(-0); final bytes = writer.takeBytes(); @@ -588,7 +588,7 @@ void main() { expect(value.isNegative, isTrue); }); - test('should throw AssertionError when Uint64 value is negative', () { + test('throw AssertionError when Uint64 value is negative', () { expect( () => writer.writeUint64(-1), throwsA( @@ -618,7 +618,7 @@ void main() { }, ); - test('should handle multiple consecutive reset calls', () { + test('handle multiple consecutive reset calls', () { writer ..writeUint8(42) ..reset() @@ -628,7 +628,7 @@ void main() { expect(writer.bytesWritten, equals(0)); }); - test('should support method chaining after reset', () { + test('support method chaining after reset', () { writer ..writeUint8(1) ..reset() @@ -640,42 +640,42 @@ void main() { }); group('Boundary values - Maximum', () { - test('should handle Uint8 maximum value (255)', () { + test('handle Uint8 maximum value (255)', () { writer.writeUint8(255); expect(writer.takeBytes(), equals([255])); }); - test('should handle Int8 maximum positive value (127)', () { + test('handle Int8 maximum positive value (127)', () { writer.writeInt8(127); expect(writer.takeBytes(), equals([127])); }); - test('should handle Int8 minimum negative value (-128)', () { + test('handle Int8 minimum negative value (-128)', () { writer.writeInt8(-128); expect(writer.takeBytes(), equals([128])); }); - test('should handle Uint16 maximum value (65535)', () { + test('handle Uint16 maximum value (65535)', () { writer.writeUint16(65535); expect(writer.takeBytes(), equals([255, 255])); }); - test('should handle Int16 maximum positive value (32767)', () { + test('handle Int16 maximum positive value (32767)', () { writer.writeInt16(32767); expect(writer.takeBytes(), equals([127, 255])); }); - test('should handle Uint32 maximum value (4294967295)', () { + test('handle Uint32 maximum value (4294967295)', () { writer.writeUint32(4294967295); expect(writer.takeBytes(), equals([255, 255, 255, 255])); }); - test('should handle Int32 maximum positive value (2147483647)', () { + test('handle Int32 maximum positive value (2147483647)', () { writer.writeInt32(2147483647); expect(writer.takeBytes(), equals([127, 255, 255, 255])); }); - test('should handle Uint64 maximum value (9223372036854775807)', () { + test('handle Uint64 maximum value (9223372036854775807)', () { writer.writeUint64(9223372036854775807); expect( writer.takeBytes(), @@ -696,49 +696,49 @@ void main() { }); group('Boundary values - Minimum', () { - test('should handle Uint8 minimum value (0)', () { + test('handle Uint8 minimum value (0)', () { writer.writeUint8(0); expect(writer.takeBytes(), equals([0])); }); - test('should handle Int8 zero value', () { + test('handle Int8 zero value', () { writer.writeInt8(0); expect(writer.takeBytes(), equals([0])); }); - test('should handle Uint16 minimum value (0)', () { + test('handle Uint16 minimum value (0)', () { writer.writeUint16(0); expect(writer.takeBytes(), equals([0, 0])); }); - test('should handle Int16 zero value', () { + test('handle Int16 zero value', () { writer.writeInt16(0); expect(writer.takeBytes(), equals([0, 0])); }); - test('should handle Uint32 minimum value (0)', () { + test('handle Uint32 minimum value (0)', () { writer.writeUint32(0); expect(writer.takeBytes(), equals([0, 0, 0, 0])); }); - test('should handle Int32 zero value', () { + test('handle Int32 zero value', () { writer.writeInt32(0); expect(writer.takeBytes(), equals([0, 0, 0, 0])); }); - test('should handle Uint64 minimum value (0)', () { + test('handle Uint64 minimum value (0)', () { writer.writeUint64(0); expect(writer.takeBytes(), equals([0, 0, 0, 0, 0, 0, 0, 0])); }); - test('should handle Int64 zero value', () { + test('handle Int64 zero value', () { writer.writeInt64(0); expect(writer.takeBytes(), equals([0, 0, 0, 0, 0, 0, 0, 0])); }); }); group('Multiple operations', () { - test('should handle multiple consecutive takeBytes calls', () { + test('handle multiple consecutive takeBytes calls', () { writer.writeUint8(1); expect(writer.takeBytes(), equals([1])); @@ -749,7 +749,7 @@ void main() { expect(writer.takeBytes(), equals([3])); }); - test('should handle toBytes followed by reset', () { + test('handle toBytes followed by reset', () { writer ..writeUint8(42) ..writeUint8(100); @@ -762,7 +762,7 @@ void main() { expect(writer.bytesWritten, equals(0)); }); - test('should handle multiple toBytes calls without modification', () { + test('handle multiple toBytes calls without modification', () { writer ..writeUint8(1) ..writeUint8(2); @@ -778,19 +778,19 @@ void main() { }); group('Byte array types', () { - test('should accept Uint8List in writeBytes', () { + test('accept Uint8List in writeBytes', () { final data = Uint8List.fromList([1, 2, 3, 4, 5]); writer.writeBytes(data); expect(writer.takeBytes(), equals([1, 2, 3, 4, 5])); }); - test('should accept regular List in writeBytes', () { + test('accept regular List in writeBytes', () { final data = [10, 20, 30, 40, 50]; writer.writeBytes(data); expect(writer.takeBytes(), equals([10, 20, 30, 40, 50])); }); - test('should handle mixed types in sequence', () { + test('handle mixed types in sequence', () { writer ..writeBytes(Uint8List.fromList([1, 2])) ..writeBytes([3, 4]) @@ -801,7 +801,7 @@ void main() { }); group('Float precision', () { - test('should handle Float32 minimum positive subnormal value', () { + test('handle Float32 minimum positive subnormal value', () { const minFloat32 = 1.4e-45; // Approximate minimum positive Float32 writer.writeFloat32(minFloat32); final bytes = writer.takeBytes(); @@ -811,7 +811,7 @@ void main() { expect(value, greaterThan(0)); }); - test('should handle Float64 minimum positive subnormal value', () { + test('handle Float64 minimum positive subnormal value', () { const minFloat64 = 5e-324; // Approximate minimum positive Float64 writer.writeFloat64(minFloat64); final bytes = writer.takeBytes(); @@ -821,7 +821,7 @@ void main() { expect(value, greaterThan(0)); }); - test('should handle Float32 maximum value', () { + test('handle Float32 maximum value', () { const maxFloat32 = 3.4028235e38; // Approximate maximum Float32 writer.writeFloat32(maxFloat32); final bytes = writer.takeBytes(); @@ -830,7 +830,7 @@ void main() { expect(reader.readFloat32(), closeTo(maxFloat32, maxFloat32 * 0.01)); }); - test('should handle Float64 maximum value', () { + test('handle Float64 maximum value', () { const maxFloat64 = 1.7976931348623157e308; // Maximum Float64 writer.writeFloat64(maxFloat64); final bytes = writer.takeBytes(); @@ -841,12 +841,12 @@ void main() { }); group('UTF-8 encoding', () { - test('should encode ASCII characters correctly', () { + test('encode ASCII characters correctly', () { writer.writeString('ABC123'); expect(writer.takeBytes(), equals([65, 66, 67, 49, 50, 51])); }); - test('should encode Cyrillic characters correctly', () { + test('encode Cyrillic characters correctly', () { writer.writeString('Привет'); final bytes = writer.takeBytes(); @@ -854,7 +854,7 @@ void main() { expect(reader.readString(bytes.length), equals('Привет')); }); - test('should encode Chinese characters correctly', () { + test('encode Chinese characters correctly', () { const str = '你好世界'; writer.writeString(str); final bytes = writer.takeBytes(); @@ -863,7 +863,7 @@ void main() { expect(reader.readString(bytes.length), equals(str)); }); - test('should encode mixed Unicode string correctly', () { + test('encode mixed Unicode string correctly', () { const str = 'Hello мир 世界 🌍'; writer.writeString(str); final bytes = writer.takeBytes(); @@ -874,7 +874,7 @@ void main() { }); group('Buffer growth strategy', () { - test('should use 1.5x growth strategy', () { + test('use 1.5x growth strategy', () { final writer = BinaryWriter(initialBufferSize: 4) // Fill initial 4 bytes ..writeUint32(0); @@ -906,7 +906,7 @@ void main() { }); group('State preservation', () { - test('should preserve written data across toBytes calls', () { + test('preserve written data across toBytes calls', () { writer.writeUint32(0x12345678); final bytes1 = writer.toBytes(); @@ -1507,37 +1507,37 @@ void main() { }); group('getUtf8Length function', () { - test('getUtf8Length with ASCII only', () { + test('with ASCII only', () { expect(getUtf8Length('Hello'), equals(5)); expect(getUtf8Length('ABCDEFGH'), equals(8)); // Fast path }); - test('getUtf8Length with empty string', () { + test('with empty string', () { expect(getUtf8Length(''), equals(0)); }); - test('getUtf8Length with 2-byte UTF-8 chars', () { + test('with 2-byte UTF-8 chars', () { expect(getUtf8Length('café'), equals(5)); // 'caf' = 3, 'é' = 2 expect(getUtf8Length('Привет'), equals(12)); // Each Cyrillic = 2 bytes }); - test('getUtf8Length with 3-byte UTF-8 chars', () { + test('with 3-byte UTF-8 chars', () { expect(getUtf8Length('世界'), equals(6)); // Each Chinese = 3 bytes expect(getUtf8Length('你好'), equals(6)); }); - test('getUtf8Length with 4-byte UTF-8 chars (emoji)', () { + test('with 4-byte UTF-8 chars (emoji)', () { expect(getUtf8Length('🌍'), equals(4)); expect(getUtf8Length('🎉'), equals(4)); expect(getUtf8Length('😀'), equals(4)); }); - test('getUtf8Length with mixed content', () { + test('with mixed content', () { // 'Hello' = 5, ', ' = 2, '世界' = 6, '! ' = 2, '🌍' = 4 expect(getUtf8Length('Hello, 世界! 🌍'), equals(19)); }); - test('getUtf8Length matches actual UTF-8 encoding', () { + test('matches actual UTF-8 encoding', () { final strings = [ 'Test', 'Тест', @@ -1558,13 +1558,13 @@ void main() { } }); - test('getUtf8Length with surrogate pairs', () { + test('with surrogate pairs', () { // Valid surrogate pair forms emoji final emoji = String.fromCharCodes([0xD83C, 0xDF0D]); // 🌍 expect(getUtf8Length(emoji), equals(4)); }); - test('getUtf8Length with malformed high surrogate', () { + test('with malformed high surrogate', () { // High surrogate (0xD800-0xDBFF) not followed by low surrogate // This triggers the malformed surrogate pair path in getUtf8Length final malformed = String.fromCharCodes([ @@ -1577,7 +1577,7 @@ void main() { ); // 3 bytes (replacement) + 1 byte (A) }); - test('getUtf8Length with lone high surrogate at end', () { + test('with lone high surrogate at end', () { // High surrogate at the end of string (also malformed) final malformed = String.fromCharCodes([ 0x0041, From c24c16a8789b48a8cbfe4f77af98b0941b97adf7 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 25 Dec 2025 17:03:23 +0200 Subject: [PATCH 09/29] refactor: Update internal state variable names in BinaryReader and BinaryWriter for consistency --- lib/src/binary_reader.dart | 90 +++++++++++++++--------------- lib/src/binary_writer.dart | 111 +++++++++++++++++++------------------ 2 files changed, 101 insertions(+), 100 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index bc662b1..458bb64 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -1,9 +1,6 @@ import 'dart:convert'; import 'dart:typed_data'; -import '../pro_binary.dart' show BinaryWriter; -import 'binary_writer.dart' show BinaryWriter; - /// A high-performance binary reader for decoding data from a byte buffer. /// /// Provides methods for reading various data types including: @@ -27,7 +24,7 @@ import 'binary_writer.dart' show BinaryWriter; /// // Check remaining data /// print('Bytes left: ${reader.availableBytes}'); /// ``` -extension type const BinaryReader._(_ReaderState _ctx) { +extension type const BinaryReader._(_ReaderState _rs) { /// Creates a new [BinaryReader] from the given byte buffer. /// /// The reader will start at position 0 and can read up to `buffer.length` @@ -36,15 +33,15 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// Returns the number of bytes remaining to be read. @pragma('vm:prefer-inline') - int get availableBytes => _ctx.length - _ctx.offset; + int get availableBytes => _rs.length - _rs.offset; /// Returns the current read position in the buffer. @pragma('vm:prefer-inline') - int get offset => _ctx.offset; + int get offset => _rs.offset; /// Returns the total length of the buffer in bytes. @pragma('vm:prefer-inline') - int get length => _ctx.length; + int get length => _rs.length; /// Reads an unsigned variable-length integer encoded using VarInt format. /// @@ -77,12 +74,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { var result = 0; var shift = 0; - final list = _ctx.list; - var offset = _ctx.offset; + final list = _rs.list; + var offset = _rs.offset; // VarInt uses up to 10 bytes for 64-bit integers for (var i = 0; i < 10; i++) { - assert(offset < _ctx.length, 'VarInt out of bounds'); + assert(offset < _rs.length, 'VarInt out of bounds'); final byte = list[offset++]; // Extract lower 7 bits and shift into position @@ -90,7 +87,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { // If MSB is 0, we've reached the last byte if ((byte & 0x80) == 0) { - _ctx.offset = offset; + _rs.offset = offset; return result; } @@ -136,7 +133,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { int readUint8() { _checkBounds(1, 'Uint8'); - return _ctx.data.getUint8(_ctx.offset++); + return _rs.data.getUint8(_rs.offset++); } /// Reads an 8-bit signed integer (-128 to 127). @@ -151,7 +148,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { int readInt8() { _checkBounds(1, 'Int8'); - return _ctx.data.getInt8(_ctx.offset++); + return _rs.data.getInt8(_rs.offset++); } /// Reads a 16-bit unsigned integer (0-65535). @@ -168,8 +165,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); - final value = _ctx.data.getUint16(_ctx.offset, endian); - _ctx.offset += 2; + final value = _rs.data.getUint16(_rs.offset, endian); + _rs.offset += 2; return value; } @@ -188,8 +185,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); - final value = _ctx.data.getInt16(_ctx.offset, endian); - _ctx.offset += 2; + final value = _rs.data.getInt16(_rs.offset, endian); + _rs.offset += 2; return value; } @@ -208,8 +205,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); - final value = _ctx.data.getUint32(_ctx.offset, endian); - _ctx.offset += 4; + final value = _rs.data.getUint32(_rs.offset, endian); + _rs.offset += 4; return value; } @@ -226,14 +223,17 @@ extension type const BinaryReader._(_ReaderState _ctx) { @pragma('vm:prefer-inline') int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); - final value = _ctx.data.getInt32(_ctx.offset, endian); - _ctx.offset += 4; + final value = _rs.data.getInt32(_rs.offset, endian); + _rs.offset += 4; return value; } /// Reads a 64-bit unsigned integer. /// - /// Note: Dart's integer precision is limited to 2^53 on web targets. + /// **Note:** Since Dart's `int` type is a signed 64-bit integer, this method + /// will return negative values for numbers greater than 2^63 - 1. + /// + /// On web targets, precision is limited to 2^53. /// /// [endian] specifies byte order (defaults to big-endian). /// @@ -246,8 +246,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { @pragma('vm:prefer-inline') int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); - final value = _ctx.data.getUint64(_ctx.offset, endian); - _ctx.offset += 8; + final value = _rs.data.getUint64(_rs.offset, endian); + _rs.offset += 8; return value; } @@ -266,8 +266,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { @pragma('vm:prefer-inline') int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); - final value = _ctx.data.getInt64(_ctx.offset, endian); - _ctx.offset += 8; + final value = _rs.data.getInt64(_rs.offset, endian); + _rs.offset += 8; return value; } @@ -285,8 +285,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); - final value = _ctx.data.getFloat32(_ctx.offset, endian); - _ctx.offset += 4; + final value = _rs.data.getFloat32(_rs.offset, endian); + _rs.offset += 4; return value; } @@ -305,8 +305,8 @@ extension type const BinaryReader._(_ReaderState _ctx) { double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); - final value = _ctx.data.getFloat64(_ctx.offset, endian); - _ctx.offset += 8; + final value = _rs.data.getFloat64(_rs.offset, endian); + _rs.offset += 8; return value; } @@ -332,10 +332,10 @@ extension type const BinaryReader._(_ReaderState _ctx) { _checkBounds(length, 'Bytes'); // Create a view of the underlying buffer without copying - final bOffset = _ctx.baseOffset; - final bytes = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); + final bOffset = _rs.baseOffset; + final bytes = _rs.data.buffer.asUint8List(bOffset + _rs.offset, length); - _ctx.offset += length; + _rs.offset += length; return bytes; } @@ -345,7 +345,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// First reads the length as a VarUint, then reads that many bytes. /// Returns a view of the underlying buffer without copying data. /// - /// This is the counterpart to [BinaryWriter.writeVarBytes]. + /// This is the counterpart to `BinaryWriter.writeVarBytes`. /// /// Example: /// ```dart @@ -397,9 +397,9 @@ extension type const BinaryReader._(_ReaderState _ctx) { _checkBounds(length, 'String'); - final bOffset = _ctx.baseOffset; - final view = _ctx.data.buffer.asUint8List(bOffset + _ctx.offset, length); - _ctx.offset += length; + final bOffset = _rs.baseOffset; + final view = _rs.data.buffer.asUint8List(bOffset + _rs.offset, length); + _rs.offset += length; return utf8.decode(view, allowMalformed: allowMalformed); } @@ -413,7 +413,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// - If true: replaces invalid sequences with U+FFFD (�) /// - If false (default): throws [FormatException] on malformed UTF-8 /// - /// This is the counterpart to [BinaryWriter.writeVarString]. + /// This is the counterpart to `BinaryWriter.writeVarString`. /// /// Example: /// ```dart @@ -456,12 +456,12 @@ extension type const BinaryReader._(_ReaderState _ctx) { return Uint8List(0); } - final peekOffset = offset ?? _ctx.offset; + final peekOffset = offset ?? _rs.offset; _checkBounds(length, 'Peek Bytes', peekOffset); - final bOffset = _ctx.baseOffset; + final bOffset = _rs.baseOffset; - return _ctx.data.buffer.asUint8List(bOffset + peekOffset, length); + return _rs.data.buffer.asUint8List(bOffset + peekOffset, length); } /// Advances the read position by the specified number of bytes. @@ -483,7 +483,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { assert(length >= 0, 'Length must be non-negative'); _checkBounds(length, 'Skip'); - _ctx.offset += length; + _rs.offset += length; } /// Resets the read position to the beginning of the buffer. @@ -491,7 +491,7 @@ extension type const BinaryReader._(_ReaderState _ctx) { /// This allows re-reading the same data without creating a new reader. @pragma('vm:prefer-inline') void reset() { - _ctx.offset = 0; + _rs.offset = 0; } /// Internal method to check if enough bytes are available to read. @@ -500,9 +500,9 @@ extension type const BinaryReader._(_ReaderState _ctx) { @pragma('vm:prefer-inline') void _checkBounds(int bytes, String type, [int? offset]) { assert( - (offset ?? _ctx.offset) + bytes <= _ctx.length, + (offset ?? _rs.offset) + bytes <= _rs.length, 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_ctx.length - _ctx.offset} bytes at offset ${_ctx.offset}', + '${_rs.length - _rs.offset} bytes at offset ${_rs.offset}', ); } } diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index dd385a6..478c3f9 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,8 +1,5 @@ import 'dart:typed_data'; -import '../pro_binary.dart' show BinaryReader; -import 'binary_reader.dart' show BinaryReader; - /// A high-performance binary writer for encoding data into a byte buffer. /// /// Provides methods for writing various data types including: @@ -30,7 +27,7 @@ import 'binary_reader.dart' show BinaryReader; /// final bytes = writer.takeBytes(); // Resets writer for reuse /// // or: final bytes = writer.toBytes(); // Keeps writer state /// ``` -extension type BinaryWriter._(_WriterState _ctx) { +extension type BinaryWriter._(_WriterState _ws) { /// Creates a new [BinaryWriter] with the specified initial buffer size. /// /// The buffer will automatically expand as needed when writing data. @@ -42,7 +39,7 @@ extension type BinaryWriter._(_WriterState _ctx) { : this._(_WriterState(initialBufferSize)); /// Returns the total number of bytes written to the buffer. - int get bytesWritten => _ctx.offset; + int get bytesWritten => _ws.offset; /// Writes an unsigned variable-length integer using VarInt encoding. /// @@ -70,16 +67,16 @@ extension type BinaryWriter._(_WriterState _ctx) { void writeVarUint(int value) { // Fast path for single-byte VarInt if (value < 0x80 && value >= 0) { - _ctx.ensureOneByte(); - _ctx.list[_ctx.offset++] = value; + _ws.ensureOneByte(); + _ws.list[_ws.offset++] = value; return; } - _ctx.ensureSize(10); + _ws.ensureSize(10); var v = value; - final list = _ctx.list; - var offset = _ctx.offset; + final list = _ws.list; + var offset = _ws.offset; while (v >= 0x80) { list[offset++] = (v & 0x7F) | 0x80; @@ -87,7 +84,7 @@ extension type BinaryWriter._(_WriterState _ctx) { } list[offset++] = v & 0x7F; - _ctx.offset = offset; + _ws.offset = offset; } /// Writes a signed variable-length integer using ZigZag encoding. @@ -116,7 +113,7 @@ extension type BinaryWriter._(_WriterState _ctx) { void writeVarInt(int value) { // ZigZag: (n << 1) ^ (n >> 63) // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 - final encoded = (value << 1) ^ (value >> 63); + final encoded = (value << 1) ^ (value >> value.bitLength); writeVarUint(encoded); } @@ -131,9 +128,9 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); - _ctx.ensureOneByte(); + _ws.ensureOneByte(); - _ctx.list[_ctx.offset++] = value; + _ws.list[_ws.offset++] = value; } /// Writes an 8-bit signed integer (-128 to 127). @@ -147,9 +144,9 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); - _ctx.ensureOneByte(); + _ws.ensureOneByte(); - _ctx.list[_ctx.offset++] = value & 0xFF; + _ws.list[_ws.offset++] = value & 0xFF; } /// Writes a 16-bit unsigned integer (0-65535). @@ -165,10 +162,10 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); - _ctx.ensureTwoBytes(); + _ws.ensureTwoBytes(); - _ctx.data.setUint16(_ctx.offset, value, endian); - _ctx.offset += 2; + _ws.data.setUint16(_ws.offset, value, endian); + _ws.offset += 2; } /// Writes a 16-bit signed integer (-32768 to 32767). @@ -184,10 +181,10 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); - _ctx.ensureTwoBytes(); + _ws.ensureTwoBytes(); - _ctx.data.setInt16(_ctx.offset, value, endian); - _ctx.offset += 2; + _ws.data.setInt16(_ws.offset, value, endian); + _ws.offset += 2; } /// Writes a 32-bit unsigned integer (0 to 4,294,967,295). @@ -203,10 +200,10 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); - _ctx.ensureFourBytes(); + _ws.ensureFourBytes(); - _ctx.data.setUint32(_ctx.offset, value, endian); - _ctx.offset += 4; + _ws.data.setUint32(_ws.offset, value, endian); + _ws.offset += 4; } /// Writes a 32-bit signed integer (-2,147,483,648 to 2,147,483,647). @@ -222,15 +219,19 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); - _ctx.ensureFourBytes(); + _ws.ensureFourBytes(); - _ctx.data.setInt32(_ctx.offset, value, endian); - _ctx.offset += 4; + _ws.data.setInt32(_ws.offset, value, endian); + _ws.offset += 4; } - /// Writes a 64-bit unsigned integer (0 to 9,223,372,036,854,775,807). + /// Writes a 64-bit unsigned integer. /// - /// Note: Dart's integer precision is limited to 2^53 for web targets. + /// **Note:** Since Dart's `int` type is a signed 64-bit integer, this method + /// is limited to the range 0 to 2^63 - 1 (9,223,372,036,854,775,807). + /// Values above this cannot be represented as positive integers in Dart. + /// + /// On web targets, precision is further limited to 2^53. /// /// [endian] specifies byte order (defaults to big-endian). /// @@ -243,10 +244,10 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeUint64(int value, [Endian endian = .big]) { _checkRange(value, 0, 9223372036854775807, 'Uint64'); - _ctx.ensureEightBytes(); + _ws.ensureEightBytes(); - _ctx.data.setUint64(_ctx.offset, value, endian); - _ctx.offset += 8; + _ws.data.setUint64(_ws.offset, value, endian); + _ws.offset += 8; } /// Writes a 64-bit signed integer. @@ -264,10 +265,10 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeInt64(int value, [Endian endian = .big]) { _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); - _ctx.ensureEightBytes(); + _ws.ensureEightBytes(); - _ctx.data.setInt64(_ctx.offset, value, endian); - _ctx.offset += 8; + _ws.data.setInt64(_ws.offset, value, endian); + _ws.offset += 8; } /// Writes a 32-bit floating-point number (IEEE 754 single precision). @@ -280,9 +281,9 @@ extension type BinaryWriter._(_WriterState _ctx) { /// ``` @pragma('vm:prefer-inline') void writeFloat32(double value, [Endian endian = .big]) { - _ctx.ensureFourBytes(); - _ctx.data.setFloat32(_ctx.offset, value, endian); - _ctx.offset += 4; + _ws.ensureFourBytes(); + _ws.data.setFloat32(_ws.offset, value, endian); + _ws.offset += 4; } /// Writes a 64-bit floating-point number (IEEE 754 double precision). @@ -295,9 +296,9 @@ extension type BinaryWriter._(_WriterState _ctx) { /// ``` @pragma('vm:prefer-inline') void writeFloat64(double value, [Endian endian = .big]) { - _ctx.ensureEightBytes(); - _ctx.data.setFloat64(_ctx.offset, value, endian); - _ctx.offset += 8; + _ws.ensureEightBytes(); + _ws.data.setFloat64(_ws.offset, value, endian); + _ws.offset += 8; } /// Writes a sequence of bytes from the given list. @@ -315,10 +316,10 @@ extension type BinaryWriter._(_WriterState _ctx) { @pragma('vm:prefer-inline') void writeBytes(List bytes, [int offset = 0, int? length]) { final len = length ?? (bytes.length - offset); - _ctx.ensureSize(len); + _ws.ensureSize(len); - _ctx.list.setRange(_ctx.offset, _ctx.offset + len, bytes, offset); - _ctx.offset += len; + _ws.list.setRange(_ws.offset, _ws.offset + len, bytes, offset); + _ws.offset += len; } /// Writes a length-prefixed byte array. @@ -326,7 +327,7 @@ extension type BinaryWriter._(_WriterState _ctx) { /// First writes the length as a VarUint, followed by the byte data. /// This is useful for serializing binary blobs of unknown size. /// - /// This is the counterpart to [BinaryReader.readVarBytes]. + /// This is the counterpart to `BinaryReader.readVarBytes`. /// /// Example: /// ```dart @@ -382,10 +383,10 @@ extension type BinaryWriter._(_WriterState _ctx) { // Pre-allocate buffer: worst case is 3 bytes per UTF-16 code unit // Most common case: 1 byte/char (ASCII) or 2-3 bytes/char (non-ASCII) // Surrogate pairs: 2 units -> 4 bytes UTF-8 (2 bytes per unit average) - _ctx.ensureSize(len * 3); + _ws.ensureSize(len * 3); - final list = _ctx.list; - var offset = _ctx.offset; + final list = _ws.list; + var offset = _ws.offset; var i = 0; while (i < len) { @@ -479,7 +480,7 @@ extension type BinaryWriter._(_WriterState _ctx) { } } - _ctx.offset = offset; + _ws.offset = offset; } /// Writes a length-prefixed UTF-8 encoded string. @@ -528,8 +529,8 @@ extension type BinaryWriter._(_WriterState _ctx) { /// ``` @pragma('vm:prefer-inline') Uint8List takeBytes() { - final result = Uint8List.sublistView(_ctx.list, 0, _ctx.offset); - _ctx._initializeBuffer(); + final result = Uint8List.sublistView(_ws.list, 0, _ws.offset); + _ws._initializeBuffer(); return result; } @@ -549,11 +550,11 @@ extension type BinaryWriter._(_WriterState _ctx) { /// final final = writer.takeBytes(); // Get all data /// ``` @pragma('vm:prefer-inline') - Uint8List toBytes() => Uint8List.sublistView(_ctx.list, 0, _ctx.offset); + Uint8List toBytes() => Uint8List.sublistView(_ws.list, 0, _ws.offset); /// Resets the writer to its initial state, discarding all written data. @pragma('vm:prefer-inline') - void reset() => _ctx._initializeBuffer(); + void reset() => _ws._initializeBuffer(); /// Handles malformed UTF-16 sequences (lone surrogates). /// @@ -566,7 +567,7 @@ extension type BinaryWriter._(_WriterState _ctx) { throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); } // Write UTF-8 encoding of U+FFFD replacement character (�) - final list = _ctx.list; + final list = _ws.list; list[offset] = 0xEF; list[offset + 1] = 0xBF; list[offset + 2] = 0xBD; From e9fe2ae7a5af879523b2f9db41c4cae3c366af0d Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 26 Dec 2025 15:08:52 +0200 Subject: [PATCH 10/29] feat: Add read and write methods for boolean values in BinaryReader and BinaryWriter --- lib/src/binary_reader.dart | 87 ++++++++++ lib/src/binary_writer.dart | 17 ++ test/binary_reader_test.dart | 306 +++++++++++++++++++++++++++++++++++ test/binary_writer_test.dart | 91 +++++++++++ 4 files changed, 501 insertions(+) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 458bb64..7b65c67 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -340,6 +340,20 @@ extension type const BinaryReader._(_ReaderState _rs) { return bytes; } + /// Reads all remaining bytes from the current position to the end of the + /// buffer. + /// + /// Returns a view of the remaining bytes without copying data. + /// Useful for reading trailing data or payloads of unknown length. + /// + /// Example: + /// ```dart + /// final payload = reader.readRemainingBytes(); + /// print('Payload length: ${payload.length}'); + /// ``` + @pragma('vm:prefer-inline') + Uint8List readRemainingBytes() => readBytes(availableBytes); + /// Reads a length-prefixed byte array. /// /// First reads the length as a VarUint, then reads that many bytes. @@ -428,6 +442,38 @@ extension type const BinaryReader._(_ReaderState _rs) { return readString(length, allowMalformed: allowMalformed); } + /// Reads a boolean value (1 byte). + /// + /// A byte value of 0 is interpreted as `false`, any non-zero value as `true`. + /// + /// Example: + /// ```dart + /// final isActive = reader.readBool(); // Read active flag + /// ``` + /// Asserts bounds in debug mode if insufficient bytes are available. + @pragma('vm:prefer-inline') + bool readBool() { + final value = readUint8(); + return value != 0; + } + + /// Checks if there are at least [length] bytes available to read. + /// + /// Returns `true` if enough bytes are available, `false` otherwise. + /// + /// Useful for conditional reads when the data format may vary. + /// Example: + /// ```dart + /// if (reader.hasBytes(4)) { + /// final value = reader.readUint32(); + /// // Process value + /// } else { + /// // Handle missing data + /// } + /// ``` + @pragma('vm:prefer-inline') + bool hasBytes(int length) => (_rs.offset + length) <= _rs.length; + /// Reads bytes without advancing the read position. /// /// This allows inspecting upcoming data without consuming it. @@ -486,6 +532,47 @@ extension type const BinaryReader._(_ReaderState _rs) { _rs.offset += length; } + /// Sets the read position to the specified byte offset. + /// + /// This allows random access within the buffer. + /// Asserts bounds in debug mode if position is out of range. + /// + /// Example: + /// ```dart + /// // Jump to a specific offset to read data + /// reader.seek(128); // Move to byte offset 128 + /// final value = reader.readUint32(); + /// ``` + @pragma('vm:prefer-inline') + void seek(int position) { + assert( + position >= 0 && position <= _rs.length, + 'Position out of bounds: $position', + ); + _rs.offset = position; + } + + /// Moves the read position backwards by the specified number of bytes. + /// + /// This allows re-reading previously read data. + /// Asserts bounds in debug mode if rewinding before the start of the buffer. + /// + /// Example: + /// ```dart + /// // Re-read the last 4 bytes + /// reader.rewind(4); + /// final value = reader.readUint32(); + /// ``` + @pragma('vm:prefer-inline') + void rewind(int length) { + assert(length >= 0, 'Length must be non-negative'); + assert( + _rs.offset - length >= 0, + 'Cannot rewind $length bytes from offset ${_rs.offset}', + ); + _rs.offset -= length; + } + /// Resets the read position to the beginning of the buffer. /// /// This allows re-reading the same data without creating a new reader. diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 478c3f9..7df83d0 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -510,6 +510,23 @@ extension type BinaryWriter._(_WriterState _ws) { writeString(value, allowMalformed: allowMalformed); } + /// Writes a boolean value as a single byte. + /// + /// `true` is written as `1` and `false` as `0`. + /// + /// Example: + /// ```dart + /// writer.writeBool(true); // Writes byte 0x01 + /// writer.writeBool(false); // Writes byte 0x00 + /// ``` + /// + @pragma('vm:prefer-inline') + // Disable lint to allow positional boolean parameter for simplicity + // ignore: avoid_positional_boolean_parameters + void writeBool(bool value) { + writeUint8(value ? 1 : 0); + } + /// Extracts all written bytes and resets the writer. /// /// After calling this method, the writer is reset and ready for reuse. diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 91acb2f..f6f68cc 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -1432,5 +1432,311 @@ void main() { expect(reader.availableBytes, equals(0)); }); }); + + group('readBool', () { + test('reads false when byte is 0', () { + final buffer = Uint8List.fromList([0x00]); + final reader = BinaryReader(buffer); + + expect(reader.readBool(), isFalse); + expect(reader.availableBytes, equals(0)); + }); + + test('reads true when byte is 1', () { + final buffer = Uint8List.fromList([0x01]); + final reader = BinaryReader(buffer); + + expect(reader.readBool(), isTrue); + expect(reader.availableBytes, equals(0)); + }); + + test('reads true when byte is any non-zero value', () { + final testValues = [1, 42, 127, 128, 255]; + for (final value in testValues) { + final buffer = Uint8List.fromList([value]); + final reader = BinaryReader(buffer); + + expect( + reader.readBool(), + isTrue, + reason: 'Value $value should be true', + ); + } + }); + + test('reads multiple boolean values correctly', () { + final buffer = Uint8List.fromList([0x01, 0x00, 0xFF, 0x00, 0x01]); + final reader = BinaryReader(buffer); + + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + expect(reader.availableBytes, equals(0)); + }); + + test('advances offset correctly', () { + final buffer = Uint8List.fromList([0x01, 0x00, 0xFF]); + final reader = BinaryReader(buffer); + + expect(reader.offset, equals(0)); + reader.readBool(); + expect(reader.offset, equals(1)); + reader.readBool(); + expect(reader.offset, equals(2)); + reader.readBool(); + expect(reader.offset, equals(3)); + }); + }); + + group('readRemainingBytes', () { + test('reads all remaining bytes from start', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([1, 2, 3, 4, 5])); + expect(reader.availableBytes, equals(0)); + }); + + test('reads remaining bytes after partial read', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..readUint16(); // Read first 2 bytes + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([3, 4, 5, 6, 7, 8])); + expect(reader.availableBytes, equals(0)); + }); + + test('returns empty list when at end of buffer', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer)..readBytes(3); // Read all bytes + final remaining = reader.readRemainingBytes(); + expect(remaining, isEmpty); + expect(reader.availableBytes, equals(0)); + }); + + test('returns empty list for empty buffer', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); + + final remaining = reader.readRemainingBytes(); + expect(remaining, isEmpty); + expect(reader.availableBytes, equals(0)); + }); + + test('returns view without copying', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint8(); // Skip first byte + final remaining = reader.readRemainingBytes(); + + // Verify it's a view by checking buffer reference + expect(remaining.buffer, equals(buffer.buffer)); + }); + }); + + group('hasBytes', () { + test('returns true when enough bytes available', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + expect(reader.hasBytes(1), isTrue); + expect(reader.hasBytes(3), isTrue); + expect(reader.hasBytes(5), isTrue); + }); + + test('returns false when not enough bytes available', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + expect(reader.hasBytes(6), isFalse); + expect(reader.hasBytes(10), isFalse); + expect(reader.hasBytes(100), isFalse); + }); + + test('returns true for exact remaining bytes', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes + expect(reader.hasBytes(3), isTrue); // Exactly 3 bytes left + expect(reader.hasBytes(4), isFalse); // Too many + }); + + test('returns true for zero bytes on non-empty buffer', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer); + + expect(reader.hasBytes(0), isTrue); + }); + + test('returns true for zero bytes on empty buffer', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); + + expect(reader.hasBytes(0), isTrue); + expect(reader.hasBytes(1), isFalse); + }); + + test('works correctly after reading', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer); + + expect(reader.hasBytes(8), isTrue); + reader.readUint32(); // Read 4 bytes + expect(reader.hasBytes(5), isFalse); + expect(reader.hasBytes(4), isTrue); + reader.readUint32(); // Read 4 more bytes + expect(reader.hasBytes(1), isFalse); + expect(reader.hasBytes(0), isTrue); + }); + + test('does not modify offset', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + expect(reader.offset, equals(0)); + reader.hasBytes(3); + expect(reader.offset, equals(0)); // Offset unchanged + reader.hasBytes(10); + expect(reader.offset, equals(0)); // Still unchanged + }); + }); + + group('seek', () { + test('sets position to beginning', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readUint32() // Move to position 4 + ..seek(0); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('sets position to middle', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(2); + expect(reader.offset, equals(2)); + expect(reader.readUint8(), equals(3)); + }); + + test('sets position to end', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(5); + expect(reader.offset, equals(5)); + expect(reader.availableBytes, equals(0)); + }); + + test('allows seeking backwards', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(4) // Move to position 4 + ..seek(1); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('allows seeking forwards', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer) + ..readUint8() // Move to position 1 + ..seek(5); + expect(reader.offset, equals(5)); + expect(reader.readUint8(), equals(6)); + }); + + test('seeking multiple times', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..seek(3); + expect(reader.offset, equals(3)); + reader.seek(1); + expect(reader.offset, equals(1)); + reader.seek(7); + expect(reader.offset, equals(7)); + reader.seek(0); + expect(reader.offset, equals(0)); + }); + + test('seeking to same position is valid', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..seek(2) + ..seek(2); + expect(reader.offset, equals(2)); + }); + }); + + group('rewind', () { + test('moves back by specified bytes', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(3) // Move to position 3 + ..rewind(2); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('rewind to beginning', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(3) + ..rewind(3); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('rewind single byte', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); // Read 2 bytes + expect(reader.offset, equals(2)); + reader.rewind(1); + expect(reader.offset, equals(1)); + expect(reader.readUint8(), equals(2)); + }); + + test('rewind zero bytes does nothing', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); + final offsetBefore = reader.offset; + reader.rewind(0); + expect(reader.offset, equals(offsetBefore)); + }); + + test('allows re-reading data', () { + final buffer = Uint8List.fromList([0x01, 0x02, 0x03, 0x04]); + final reader = BinaryReader(buffer); + + final first = reader.readUint32(); + expect(first, equals(0x01020304)); + + reader.rewind(4); + final second = reader.readUint32(); + expect(second, equals(0x01020304)); + expect(second, equals(first)); + }); + + test('multiple rewinds', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..readBytes(5); // Position 5 + expect(reader.offset, equals(5)); + + reader.rewind(2); // Position 3 + expect(reader.offset, equals(3)); + + reader.rewind(1); // Position 2 + expect(reader.offset, equals(2)); + + expect(reader.readUint8(), equals(3)); + }); + + test('rewind and seek together', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer) + ..seek(5) + ..rewind(2); + expect(reader.offset, equals(3)); + + reader.rewind(3); + expect(reader.offset, equals(0)); + }); + }); }); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 73ac711..006686a 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -1628,5 +1628,96 @@ void main() { expect(bytes, equals([42, 43])); }); }); + + group('writeBool', () { + test('writes true as 0x01', () { + writer.writeBool(true); + expect(writer.takeBytes(), equals([0x01])); + }); + + test('writes false as 0x00', () { + writer.writeBool(false); + expect(writer.takeBytes(), equals([0x00])); + }); + + test('writes multiple boolean values correctly', () { + writer + ..writeBool(true) + ..writeBool(false) + ..writeBool(true) + ..writeBool(true) + ..writeBool(false); + + expect(writer.takeBytes(), equals([0x01, 0x00, 0x01, 0x01, 0x00])); + }); + + test('can be read back with readBool', () { + writer + ..writeBool(true) + ..writeBool(false) + ..writeBool(true); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + }); + + test('updates bytesWritten correctly', () { + expect(writer.bytesWritten, equals(0)); + + writer.writeBool(true); + expect(writer.bytesWritten, equals(1)); + + writer.writeBool(false); + expect(writer.bytesWritten, equals(2)); + + writer.writeBool(true); + expect(writer.bytesWritten, equals(3)); + }); + + test('can be mixed with other write operations', () { + writer + ..writeUint8(42) + ..writeBool(true) + ..writeUint16(1000) + ..writeBool(false) + ..writeInt32(-500); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readUint8(), equals(42)); + expect(reader.readBool(), isTrue); + expect(reader.readUint16(), equals(1000)); + expect(reader.readBool(), isFalse); + expect(reader.readInt32(), equals(-500)); + }); + + test('expands buffer when needed', () { + // Write many booleans to trigger buffer expansion + for (var i = 0; i < 200; i++) { + writer.writeBool(i.isEven); + } + + final bytes = writer.takeBytes(); + expect(bytes.length, equals(200)); + + final reader = BinaryReader(bytes); + for (var i = 0; i < 200; i++) { + expect(reader.readBool(), equals(i.isEven)); + } + }); + + test('resets correctly after takeBytes', () { + writer + ..writeBool(true) + ..takeBytes() + ..writeBool(false); + expect(writer.takeBytes(), equals([0x00])); + }); + }); }); } From f64068f5b66293877f01975509f5846366d8d8c9 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 26 Dec 2025 15:15:37 +0200 Subject: [PATCH 11/29] feat: Add tests for BinaryReader and BinaryWriter to validate error handling and state management --- test/binary_reader_test.dart | 100 +++++++++++++++++++++++++++++++++-- test/binary_writer_test.dart | 21 ++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index f6f68cc..85fb0ef 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -1488,6 +1488,19 @@ void main() { reader.readBool(); expect(reader.offset, equals(3)); }); + + test('throws when reading from empty buffer', () { + final buffer = Uint8List.fromList([]); + final reader = BinaryReader(buffer); + + expect(reader.readBool, throwsA(isA())); + }); + + test('throws when no bytes available', () { + final buffer = Uint8List.fromList([0x01]); + final reader = BinaryReader(buffer)..readBool(); // Consume the byte + expect(reader.readBool, throwsA(isA())); + }); }); group('readRemainingBytes', () { @@ -1502,7 +1515,10 @@ void main() { test('reads remaining bytes after partial read', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - final reader = BinaryReader(buffer)..readUint16(); // Read first 2 bytes + final reader = BinaryReader(buffer) + // Read first 2 bytes + ..readUint16(); + final remaining = reader.readRemainingBytes(); expect(remaining, equals([3, 4, 5, 6, 7, 8])); expect(reader.availableBytes, equals(0)); @@ -1525,14 +1541,35 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('returns view without copying', () { + test('is zero-copy operation', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); - final reader = BinaryReader(buffer)..readUint8(); // Skip first byte - final remaining = reader.readRemainingBytes(); + final reader = BinaryReader(buffer) + // Skip first byte + ..readUint8(); + final remaining = reader.readRemainingBytes(); // Verify it's a view by checking buffer reference expect(remaining.buffer, equals(buffer.buffer)); }); + + test('can be called multiple times at end', () { + final buffer = Uint8List.fromList([1, 2, 3]); + final reader = BinaryReader(buffer)..readBytes(3); + + final first = reader.readRemainingBytes(); + final second = reader.readRemainingBytes(); + + expect(first, isEmpty); + expect(second, isEmpty); + }); + + test('works correctly after seek', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(2); + + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([3, 4, 5])); + }); }); group('hasBytes', () { @@ -1599,6 +1636,25 @@ void main() { reader.hasBytes(10); expect(reader.offset, equals(0)); // Still unchanged }); + + test('works correctly after seek', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..seek(3); + + expect(reader.hasBytes(2), isTrue); + expect(reader.hasBytes(3), isFalse); + expect(reader.offset, equals(3)); // Unchanged + }); + + test('works correctly after rewind', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer) + ..readBytes(4) + ..rewind(2); + + expect(reader.hasBytes(3), isTrue); + expect(reader.hasBytes(4), isFalse); + }); }); group('seek', () { @@ -1662,6 +1718,21 @@ void main() { ..seek(2); expect(reader.offset, equals(2)); }); + + test('throws on negative position', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + expect(() => reader.seek(-1), throwsA(isA())); + }); + + test('throws when seeking beyond buffer', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + expect(() => reader.seek(6), throwsA(isA())); + expect(() => reader.seek(100), throwsA(isA())); + }); }); group('rewind', () { @@ -1737,6 +1808,27 @@ void main() { reader.rewind(3); expect(reader.offset, equals(0)); }); + + test('throws when rewinding beyond start', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readUint16(); // offset = 2 + + expect(() => reader.rewind(3), throwsA(isA())); + }); + + test('throws when rewinding from start', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer); + + expect(() => reader.rewind(1), throwsA(isA())); + }); + + test('throws on negative length', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readBytes(3); + + expect(() => reader.rewind(-1), throwsA(isA())); + }); }); }); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 006686a..0595719 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -1718,6 +1718,27 @@ void main() { ..writeBool(false); expect(writer.takeBytes(), equals([0x00])); }); + + test('works correctly with toBytes', () { + writer.writeBool(true); + final snapshot1 = writer.toBytes(); + expect(snapshot1, equals([0x01])); + + writer.writeBool(false); + final snapshot2 = writer.toBytes(); + expect(snapshot2, equals([0x01, 0x00])); + }); + + test('works correctly with reset', () { + writer + ..writeBool(true) + ..writeBool(false) + ..reset() + ..writeBool(false) + ..writeBool(true); + + expect(writer.toBytes(), equals([0x00, 0x01])); + }); }); }); } From f1ca0f43bc8146825e38a1a0fcb0dda85dae8284 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 26 Dec 2025 15:20:46 +0200 Subject: [PATCH 12/29] feat: Add tests for BinaryReader to validate error handling for VarUint and VarInt --- lib/src/binary_writer.dart | 2 +- test/binary_reader_test.dart | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 7df83d0..f617b08 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -519,7 +519,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeBool(true); // Writes byte 0x01 /// writer.writeBool(false); // Writes byte 0x00 /// ``` - /// + /// @pragma('vm:prefer-inline') // Disable lint to allow positional boolean parameter for simplicity // ignore: avoid_positional_boolean_parameters diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 85fb0ef..61e45bd 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -382,6 +382,42 @@ void main() { expect(reader.availableBytes, equals(0)); }); + test('readVarUint throws on truncated varint', () { + // VarInt with continuation bit set but no following byte + final buffer = Uint8List.fromList([0x80]); // MSB=1, expects more bytes + final reader = BinaryReader(buffer); + + expect(reader.readVarUint, throwsA(isA())); + }); + + test('readVarUint throws on incomplete multi-byte varint', () { + // Two-byte VarInt with only first byte + final buffer = Uint8List.fromList([0xFF]); // All continuation bits set + final reader = BinaryReader(buffer); + + expect(reader.readVarUint, throwsA(isA())); + }); + + test('readVarUint throws FormatException on too long varint', () { + // 11 bytes with all continuation bits set (exceeds 10-byte limit) + final buffer = Uint8List.fromList([ + 0x80, 0x80, 0x80, 0x80, 0x80, // + 0x80, 0x80, 0x80, 0x80, 0x80, // + 0x80, // 11th byte + ]); + final reader = BinaryReader(buffer); + + expect(reader.readVarUint, throwsA(isA())); + }); + + test('readVarInt throws on truncated zigzag', () { + // Truncated VarInt (continuation bit set but no next byte) + final buffer = Uint8List.fromList([0x80]); + final reader = BinaryReader(buffer); + + expect(reader.readVarInt, throwsA(isA())); + }); + test('readBytes', () { final data = [0x01, 0x02, 0x03, 0x04, 0x05]; final buffer = Uint8List.fromList(data); From a89c0507d17592426c9c7c950b1d9a871c921d4e Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 26 Dec 2025 15:33:12 +0200 Subject: [PATCH 13/29] feat: Enhance BinaryWriter with offset and length validation in writeBytes method and add corresponding tests --- lib/src/binary_reader.dart | 5 +++- lib/src/binary_writer.dart | 10 ++++++- test/binary_writer_test.dart | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 7b65c67..150f486 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -472,7 +472,10 @@ extension type const BinaryReader._(_ReaderState _rs) { /// } /// ``` @pragma('vm:prefer-inline') - bool hasBytes(int length) => (_rs.offset + length) <= _rs.length; + bool hasBytes(int length) { + assert(length >= 0, 'Length must be non-negative'); + return (_rs.offset + length) <= _rs.length; + } /// Reads bytes without advancing the read position. /// diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index f617b08..ea54d54 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -315,7 +315,14 @@ extension type BinaryWriter._(_WriterState _ws) { /// ``` @pragma('vm:prefer-inline') void writeBytes(List bytes, [int offset = 0, int? length]) { + assert(offset >= 0, 'Offset must be non-negative'); + assert(offset <= bytes.length, 'Offset exceeds list length'); + final len = length ?? (bytes.length - offset); + + assert(len >= 0, 'Length must be non-negative'); + assert(offset + len <= bytes.length, 'Offset + length exceeds list length'); + _ws.ensureSize(len); _ws.list.setRange(_ws.offset, _ws.offset + len, bytes, offset); @@ -605,7 +612,8 @@ extension type BinaryWriter._(_WriterState _ws) { /// Separated from the extension type to allow efficient inline operations. final class _WriterState { _WriterState(int initialBufferSize) - : _size = initialBufferSize, + : assert(initialBufferSize > 0, 'Initial buffer size must be positive'), + _size = initialBufferSize, capacity = initialBufferSize, offset = 0, list = Uint8List(initialBufferSize) { diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 0595719..90abb62 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -798,6 +798,63 @@ void main() { expect(writer.takeBytes(), equals([1, 2, 3, 4, 5])); }); + + test('writeBytes with offset parameter', () { + final data = [1, 2, 3, 4, 5]; + writer.writeBytes(data, 2); // Write from index 2: [3, 4, 5] + expect(writer.takeBytes(), equals([3, 4, 5])); + }); + + test('writeBytes with offset and length parameters', () { + final data = [1, 2, 3, 4, 5]; + writer.writeBytes(data, 1, 3); // Write [2, 3, 4] + expect(writer.takeBytes(), equals([2, 3, 4])); + }); + + test('writeBytes with offset at end', () { + final data = [1, 2, 3, 4, 5]; + writer.writeBytes(data, 5); // Write from end (empty) + expect(writer.takeBytes(), equals([])); + }); + + test('writeBytes with zero length', () { + final data = [1, 2, 3, 4, 5]; + writer.writeBytes(data, 0, 0); // Write 0 bytes + expect(writer.takeBytes(), equals([])); + }); + + test('writeBytes throws on negative offset', () { + final data = [1, 2, 3, 4, 5]; + expect( + () => writer.writeBytes(data, -1), + throwsA(isA()), + ); + }); + + test('writeBytes throws on negative length', () { + final data = [1, 2, 3, 4, 5]; + expect( + () => writer.writeBytes(data, 0, -1), + throwsA(isA()), + ); + }); + + test('writeBytes throws when offset exceeds list length', () { + final data = [1, 2, 3]; + expect( + () => writer.writeBytes(data, 4), + throwsA(isA()), + ); + }); + + test('writeBytes throws when offset + length exceeds list', () { + final data = [1, 2, 3, 4, 5]; + expect( + // offset 2 + length 5 > list length 5 + () => writer.writeBytes(data, 2, 5), + throwsA(isA()), + ); + }); }); group('Float precision', () { From 859c71f293c380f1f5a1908862197bcc89d24bfd Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 29 Dec 2025 13:46:23 +0200 Subject: [PATCH 14/29] feat: Optimize VarInt and VarUint encoding in BinaryWriter and add comprehensive tests for boundary cases --- lib/src/binary_reader.dart | 25 ++- lib/src/binary_writer.dart | 272 ++++++++++++++++++++--- test/binary_writer_performance_test.dart | 51 +++++ test/binary_writer_test.dart | 78 +++++++ 4 files changed, 388 insertions(+), 38 deletions(-) diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 150f486..5ad1750 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -71,21 +71,30 @@ extension type const BinaryReader._(_ReaderState _rs) { /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') int readVarUint() { - var result = 0; - var shift = 0; + assert(_rs.offset < _rs.length, 'VarInt out of bounds'); final list = _rs.list; + final len = _rs.length; var offset = _rs.offset; - // VarInt uses up to 10 bytes for 64-bit integers - for (var i = 0; i < 10; i++) { - assert(offset < _rs.length, 'VarInt out of bounds'); - final byte = list[offset++]; + // Fast path: single byte (0-127) — most common case + var byte = list[offset++]; + if ((byte & 0x80) == 0) { + _rs.offset = offset; + return byte; + } + + // Multi-byte VarInt (optimized for 2-3 byte case) + var result = byte & 0x7f; + var shift = 7; + + // Process remaining bytes: up to 9 more (total 10 max) + for (var i = 1; i < 10; i++) { + assert(offset < len, 'VarInt out of bounds'); + byte = list[offset++]; - // Extract lower 7 bits and shift into position result |= (byte & 0x7f) << shift; - // If MSB is 0, we've reached the last byte if ((byte & 0x80) == 0) { _rs.offset = offset; return result; diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index ea54d54..0def1e3 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -64,26 +64,61 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeVarUint(1000000); // 3 bytes /// ``` @pragma('vm:prefer-inline') + @pragma('vm:prefer-inline') void writeVarUint(int value) { - // Fast path for single-byte VarInt + // Fast path: single-byte (0-127) + var offset = _ws.offset; if (value < 0x80 && value >= 0) { _ws.ensureOneByte(); - _ws.list[_ws.offset++] = value; + _ws.list[offset++] = value; + _ws.offset = offset; return; } + // Slow path: multi-byte VarInt + final list = _ws.list; _ws.ensureSize(10); - var v = value; - final list = _ws.list; - var offset = _ws.offset; + // First byte (always has continuation bit) + list[offset++] = (value & 0x7F) | 0x80; + var v = value >>> 7; + // Unrolled 2-byte case (covers 0-16383, ~90% of real-world values) + if (v < 0x80) { + list[offset++] = v; + _ws.offset = offset; + return; + } + + // Second byte + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + + // Unrolled 3-byte case (covers 0-2097151) + if (v < 0x80) { + list[offset++] = v; + _ws.offset = offset; + return; + } + + // Third byte + list[offset++] = (v & 0x7F) | 0x80; + v >>>= 7; + + // Unrolled 4-byte case (covers 0-268435455, ~99.9% of 32-bit values) + if (v < 0x80) { + list[offset++] = v; + _ws.offset = offset; + return; + } + + // Generic loop for remaining bytes (rare large 64-bit numbers) while (v >= 0x80) { list[offset++] = (v & 0x7F) | 0x80; v >>>= 7; } - list[offset++] = v & 0x7F; + list[offset++] = v; // Last byte without continuation bit _ws.offset = offset; } @@ -113,7 +148,7 @@ extension type BinaryWriter._(_WriterState _ws) { void writeVarInt(int value) { // ZigZag: (n << 1) ^ (n >> 63) // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 - final encoded = (value << 1) ^ (value >> value.bitLength); + final encoded = (value << 1) ^ (value >> 63); writeVarUint(encoded); } @@ -635,6 +670,8 @@ final class _WriterState { /// Initial buffer size. final int _size; + var _isInPool = false; + @pragma('vm:prefer-inline') void _initializeBuffer() { list = Uint8List(_size); @@ -714,7 +751,7 @@ final class _WriterState { /// 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 8 ASCII characters at once. +/// up to 4 ASCII characters at once. /// /// Useful for: /// - Pre-allocating buffers of the correct size @@ -722,7 +759,7 @@ final class _WriterState { /// - Validating string length constraints /// /// Performance: -/// - ASCII strings: ~8 bytes per loop iteration +/// - ASCII strings: ~4 bytes per loop iteration /// - Mixed content: Falls back to character-by-character analysis /// /// Example: @@ -734,43 +771,39 @@ final class _WriterState { /// /// @param s The input string. /// @return The number of bytes needed for UTF-8 encoding. -int getUtf8Length(String s) { - if (s.isEmpty) { +int getUtf8Length(String value) { + if (value.isEmpty) { return 0; } - final len = s.length; + final len = value.length; var bytes = 0; var i = 0; while (i < len) { - final c = s.codeUnitAt(i); + final char = value.codeUnitAt(i); // ASCII fast path - if (c < 0x80) { - // Process 8 ASCII characters at a time - final end = len - 8; + if (char < 0x80) { + // Process 4 ASCII characters at a time + final end = len - 4; while (i <= end) { final mask = - s.codeUnitAt(i) | - s.codeUnitAt(i + 1) | - s.codeUnitAt(i + 2) | - s.codeUnitAt(i + 3) | - s.codeUnitAt(i + 4) | - s.codeUnitAt(i + 5) | - s.codeUnitAt(i + 6) | - s.codeUnitAt(i + 7); + value.codeUnitAt(i) | + value.codeUnitAt(i + 1) | + value.codeUnitAt(i + 2) | + value.codeUnitAt(i + 3); if (mask >= 0x80) { break; } - i += 8; - bytes += 8; + i += 4; + bytes += 4; } // Handle remaining ASCII characters - while (i < len && s.codeUnitAt(i) < 0x80) { + while (i < len && value.codeUnitAt(i) < 0x80) { i++; bytes++; } @@ -781,13 +814,13 @@ int getUtf8Length(String s) { } // 2-byte sequence - if (c < 0x800) { + if (char < 0x800) { bytes += 2; i++; } // 3-byte sequence - else if (c >= 0xD800 && c <= 0xDBFF && i + 1 < len) { - final next = s.codeUnitAt(i + 1); + else if (char >= 0xD800 && char <= 0xDBFF && i + 1 < len) { + final next = value.codeUnitAt(i + 1); if (next >= 0xDC00 && next <= 0xDFFF) { bytes += 4; i += 2; @@ -806,3 +839,182 @@ int getUtf8Length(String s) { return bytes; } + +// Disable lint to allow static-only class for pooling +// ignore: avoid_classes_with_only_static_members +/// 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], [getStatistics] for pool monitoring +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; + + /// 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.toBytes(); + /// } finally { + /// BinaryWriterPool.release(writer); + /// } + /// ``` + /// + /// Returns: A [BinaryWriter] ready for use. + static BinaryWriter acquire() { + if (_pool.isNotEmpty) { + final state = _pool.removeLast().._isInPool = false; + return BinaryWriter._(state); + } + + return BinaryWriter(initialBufferSize: _defaultBufferSize); + } + + /// 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.writeString('invalid'); + /// ``` + /// + /// 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); + } + } + + /// 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 + /// + /// Example: + /// ```dart + /// final stats = BinaryWriterPool.getStatistics(); + /// print('Pooled writers: ${stats['pooled']}'); // 5 + /// ``` + static Map getStatistics() => { + 'pooled': _pool.length, + 'maxPoolSize': _maxPoolSize, + 'defaultBufferSize': _defaultBufferSize, + 'maxReusableCapacity': _maxReusableCapacity, + }; + + /// 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() => _pool.clear(); +} diff --git a/test/binary_writer_performance_test.dart b/test/binary_writer_performance_test.dart index 411d4f4..dee7b29 100644 --- a/test/binary_writer_performance_test.dart +++ b/test/binary_writer_performance_test.dart @@ -103,6 +103,57 @@ class BinaryWriterBenchmark extends BenchmarkBase { } } +class BinaryWriterVarIntBenchmark extends BenchmarkBase { + BinaryWriterVarIntBenchmark() : super('BinaryWriter performance test'); + + late final BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer + ..writeVarInt(42) + ..writeVarInt(-42) + ..writeVarInt(512) + ..writeVarInt(-512) + ..writeVarUint(65535) + ..writeVarUint(100) + ..writeVarInt(-32768) + ..writeVarInt(32768) + ..writeVarUint(4294967295) + ..writeVarUint(100) + ..writeVarInt(-2147483648) + ..writeVarInt(2147483647) + ..writeVarUint(9223372036854775807) + ..writeVarUint(1000) + ..writeVarInt(-9223372036854775808) + ..writeVarInt(9223372036854775807); + + final bytes = writer.takeBytes(); + + if (writer.bytesWritten != 0) { + throw StateError('bytesWritten should be reset to 0 after takeBytes()'); + } + + if (bytes.length != 63) { + throw StateError('Unexpected byte length: ${bytes.length}'); + } + } + } + + @override + void exercise() => run(); + static void main() { + BinaryWriterVarIntBenchmark().report(); + } +} + void main() { BinaryWriterBenchmark.main(); + BinaryWriterVarIntBenchmark.main(); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index 90abb62..e211924 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -181,6 +181,84 @@ void main() { expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); }); + test('writeVarUint fast path boundary: 0', () { + // 0 is unsigned and should use fast path (single byte) + writer.writeVarUint(0); + expect(writer.takeBytes(), [0]); + }); + + test('writeVarUint fast path boundary: 127 (max single byte)', () { + // 127 (0x7F) is the last value where MSB is not set + writer.writeVarUint(127); + expect(writer.takeBytes(), [127]); + }); + + test('writeVarUint multi-byte boundary: 128 (min two bytes)', () { + // 128 (0x80) requires 2 bytes because MSB is set + writer.writeVarUint(128); + expect(writer.takeBytes(), [0x80, 0x01]); + }); + + test('writeVarInt fast path: ZigZag encodes small values correctly', () { + // ZigZag(0) = 0 → single byte + writer.writeVarInt(0); + expect(writer.toBytes(), [0]); + + writer + ..reset() + // ZigZag(1) = 2 → single byte + ..writeVarInt(1); + expect(writer.toBytes(), [2]); + + writer + ..reset() + // ZigZag(-1) = 1 → single byte + ..writeVarInt(-1); + expect(writer.toBytes(), [1]); + }); + + test('writeVarInt multi-byte: ZigZag crosses boundary correctly', () { + // ZigZag(64) = 128 → requires 2 bytes (MSB set) + writer.writeVarInt(64); + expect(writer.takeBytes(), [0x80, 0x01]); + + // ZigZag(-64) = 127 → single byte + writer.writeVarInt(-64); + expect(writer.takeBytes(), [127]); + + // ZigZag(-65) = 129 → requires 2 bytes + writer.writeVarInt(-65); + expect(writer.takeBytes(), [0x81, 0x01]); + }); + + test( + 'writeVarUint with negative value must not use fast path ' + '(regression test)', + () { + // CRITICAL: writeVarUint(-1) must NOT use fast path + // Negative numbers: -1 as bits = 0xFFFFFFFF... + // -1 < 0x80 is FALSE, so it should use slow path + // This verifies the `value >= 0` check is necessary + + writer.writeVarUint(-1); + final bytes = writer.takeBytes(); + + // Without `value >= 0` check, -1 might be incorrectly encoded as 1 byte + // With check: -1 triggers slow path and encodes as 10 bytes + expect( + bytes.length, + 10, + reason: 'Negative number should use multi-byte path', + ); + expect( + bytes[0], + 0xFF, + reason: 'First byte should have continuation bit set', + ); + expect(bytes[9], 0x01, reason: 'Last byte should be continuation end'); + }, + ); + test('write byte array correctly', () { writer.writeBytes([1, 2, 3, 4, 5]); expect(writer.takeBytes(), [1, 2, 3, 4, 5]); From 1964bbf223f0b88531bbc8f7fd7786da9951c6b0 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 30 Dec 2025 13:28:03 +0200 Subject: [PATCH 15/29] feat: Improve error handling in BinaryReader and BinaryWriter, update tests for RangeError, and add benchmark configuration --- .github/workflows/test.yml | 4 +- README.md | 26 +++++- benchmark_baseline.json | 14 +++ dart_test.yaml | 7 ++ lib/src/binary_reader.dart | 72 ++++++++++----- lib/src/binary_writer.dart | 40 ++++++-- test/binary_reader_performance_test.dart | 53 ++--------- test/binary_reader_test.dart | 112 +++++++++++------------ test/binary_writer_performance_test.dart | 16 ++-- test/binary_writer_test.dart | 43 +++++---- 10 files changed, 226 insertions(+), 161 deletions(-) create mode 100644 benchmark_baseline.json create mode 100644 dart_test.yaml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab8259c..b2e1523 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,13 +39,13 @@ jobs: run: dart analyze --fatal-infos - name: Run tests - run: dart test + run: dart test -x benchmark - name: Run tests with coverage if: matrix.os == 'ubuntu-latest' && matrix.sdk == 'stable' run: | dart pub global activate coverage - dart pub global run coverage:test_with_coverage + dart pub global run coverage:test_with_coverage -- -x benchmark - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.sdk == 'stable' diff --git a/README.md b/README.md index 4cd8ba9..9005de6 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,24 @@ writer.writeVarInt(-1000); // 2 bytes **Use VarUint** for: lengths, counts, IDs **Use VarInt** for: deltas, offsets, signed values +## Encoding Efficiency + +VarInt encoding significantly reduces payload size for small values: + +| Value | VarInt | Fixed Uint32 | Savings | +| ------- | -------- | -------------- | --------- | +| 0 | 1 byte | 4 bytes | **75%** | +| 42 | 1 byte | 4 bytes | **75%** | +| 127 | 1 byte | 4 bytes | **75%** | +| 128 | 2 bytes | 4 bytes | **50%** | +| 300 | 2 bytes | 4 bytes | **50%** | +| 16,384 | 3 bytes | 4 bytes | **25%** | +| 1,000,000 | 3 bytes | 4 bytes | **25%** | +| 268,435,455 | 4 bytes | 4 bytes | **0%** | + +**Use VarInt for:** lengths, counts, sizes, small IDs +**Use fixed-width for:** timestamps, coordinates, fixed-size IDs + ## Tips & Best Practices **Buffer Sizing**: Writer starts at 128 bytes and auto-expands. For large data, set initial size: @@ -199,7 +217,7 @@ writer.writeString(text); writer.writeString(text); ``` -**Error Handling**: Bounds checks run in debug mode. Catch errors for user input: +**Error Handling**: Invalid data and out-of-bounds reads/writes throw `RangeError`. Catch errors for user input: ```dart try { @@ -223,8 +241,10 @@ Comprehensive test suite with **336+ tests** covering: Run tests: ```bash -dart test # Run all tests -dart test test/varint_test.dart # Run VarInt-specific tests +dart test -x benchmark # Run unit/integration tests (skip benchmarks) +dart test -t benchmark # Run performance benchmarks only +dart test # Run everything (including benchmarks) +dart test test/binary_reader_test.dart # Run a single test file dart analyze # Check code quality ``` diff --git a/benchmark_baseline.json b/benchmark_baseline.json new file mode 100644 index 0000000..27de111 --- /dev/null +++ b/benchmark_baseline.json @@ -0,0 +1,14 @@ +{ + "schema": 1, + "createdAt": "2025-12-29T13:09:01.837273Z", + "dart": "Dart SDK version: 3.10.4 (stable) (Tue Dec 9 00:01:55 2025 -0800) on \"linux_x64\"", + "runs": 10, + "warmup": 2, + "tag": "benchmark", + "benchmarkRegex": null, + "mediansUs": { + "BinaryReader performance test": 3706.0389090909093, + "BinaryWriter performance test": 150.3888255564516, + "GetStringLength performance test": 6697.887155444721 + } +} \ No newline at end of file diff --git a/dart_test.yaml b/dart_test.yaml new file mode 100644 index 0000000..6c78dc3 --- /dev/null +++ b/dart_test.yaml @@ -0,0 +1,7 @@ +# Test configuration for package:test / dart test. +# +# Declare custom tags to avoid warnings and to document intent. + +tags: + benchmark: + description: Performance/benchmark tests (excluded from CI by default). diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 5ad1750..0e59057 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -71,12 +71,14 @@ extension type const BinaryReader._(_ReaderState _rs) { /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') int readVarUint() { - assert(_rs.offset < _rs.length, 'VarInt out of bounds'); - final list = _rs.list; final len = _rs.length; var offset = _rs.offset; + if (offset >= len) { + throw RangeError('VarInt out of bounds: offset=$offset length=$len'); + } + // Fast path: single byte (0-127) — most common case var byte = list[offset++]; if ((byte & 0x80) == 0) { @@ -90,7 +92,11 @@ extension type const BinaryReader._(_ReaderState _rs) { // Process remaining bytes: up to 9 more (total 10 max) for (var i = 1; i < 10; i++) { - assert(offset < len, 'VarInt out of bounds'); + if (offset >= len) { + throw RangeError( + 'VarInt out of bounds: offset=$offset length=$len (truncated)', + ); + } byte = list[offset++]; result |= (byte & 0x7f) << shift; @@ -337,7 +343,9 @@ extension type const BinaryReader._(_ReaderState _rs) { /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') Uint8List readBytes(int length) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } _checkBounds(length, 'Bytes'); // Create a view of the underlying buffer without copying @@ -444,7 +452,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// print(text); // 'Hello, 世界! 🌍' /// ``` /// - /// Throws [AssertionError] if attempting to read past buffer end. + /// Throws [RangeError] if attempting to read past buffer end. @pragma('vm:prefer-inline') String readVarString({bool allowMalformed = false}) { final length = readVarUint(); @@ -482,7 +490,9 @@ extension type const BinaryReader._(_ReaderState _rs) { /// ``` @pragma('vm:prefer-inline') bool hasBytes(int length) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } return (_rs.offset + length) <= _rs.length; } @@ -508,7 +518,9 @@ extension type const BinaryReader._(_ReaderState _rs) { /// ``` @pragma('vm:prefer-inline') Uint8List peekBytes(int length, [int? offset]) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } if (length == 0) { return Uint8List(0); @@ -538,7 +550,9 @@ extension type const BinaryReader._(_ReaderState _rs) { /// reader.skip(payloadSize); /// ``` void skip(int length) { - assert(length >= 0, 'Length must be non-negative'); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } _checkBounds(length, 'Skip'); _rs.offset += length; @@ -557,10 +571,9 @@ extension type const BinaryReader._(_ReaderState _rs) { /// ``` @pragma('vm:prefer-inline') void seek(int position) { - assert( - position >= 0 && position <= _rs.length, - 'Position out of bounds: $position', - ); + if (position < 0 || position > _rs.length) { + throw RangeError.range(position, 0, _rs.length, 'position'); + } _rs.offset = position; } @@ -577,11 +590,14 @@ extension type const BinaryReader._(_ReaderState _rs) { /// ``` @pragma('vm:prefer-inline') void rewind(int length) { - assert(length >= 0, 'Length must be non-negative'); - assert( - _rs.offset - length >= 0, - 'Cannot rewind $length bytes from offset ${_rs.offset}', - ); + if (length < 0) { + throw RangeError.value(length, 'length', 'Length must be non-negative'); + } + if (_rs.offset - length < 0) { + throw RangeError( + 'Cannot rewind $length bytes from offset ${_rs.offset}', + ); + } _rs.offset -= length; } @@ -598,11 +614,23 @@ extension type const BinaryReader._(_ReaderState _rs) { /// Throws an assertion error in debug mode if not enough bytes. @pragma('vm:prefer-inline') void _checkBounds(int bytes, String type, [int? offset]) { - assert( - (offset ?? _rs.offset) + bytes <= _rs.length, - 'Not enough bytes to read $type: required $bytes bytes, available ' - '${_rs.length - _rs.offset} bytes at offset ${_rs.offset}', - ); + if (bytes < 0) { + throw RangeError.value(bytes, 'bytes', 'Bytes must be non-negative'); + } + + final start = offset ?? _rs.offset; + final end = start + bytes; + + if (start < 0 || start > _rs.length) { + throw RangeError.range(start, 0, _rs.length, 'offset'); + } + + if (end > _rs.length) { + throw RangeError( + 'Not enough bytes to read $type: required $bytes bytes, available ' + '${_rs.length - _rs.offset} bytes at offset ${_rs.offset}', + ); + } } } diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 0def1e3..08d7b73 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -350,13 +350,25 @@ extension type BinaryWriter._(_WriterState _ws) { /// ``` @pragma('vm:prefer-inline') void writeBytes(List bytes, [int offset = 0, int? length]) { - assert(offset >= 0, 'Offset must be non-negative'); - assert(offset <= bytes.length, 'Offset exceeds list length'); + if (offset < 0) { + throw RangeError.value(offset, 'offset', 'Offset must be non-negative'); + } + if (offset > bytes.length) { + throw RangeError.range(offset, 0, bytes.length, 'offset'); + } final len = length ?? (bytes.length - offset); - assert(len >= 0, 'Length must be non-negative'); - assert(offset + len <= bytes.length, 'Offset + length exceeds list length'); + if (len < 0) { + throw RangeError.value(len, 'length', 'Length must be non-negative'); + } + if (offset + len > bytes.length) { + throw RangeError( + 'Offset + length exceeds list length: ' + 'offset=$offset length=$len ' + 'listLength=${bytes.length}', + ); + } _ws.ensureSize(len); @@ -647,14 +659,26 @@ extension type BinaryWriter._(_WriterState _ws) { /// Separated from the extension type to allow efficient inline operations. final class _WriterState { _WriterState(int initialBufferSize) - : assert(initialBufferSize > 0, 'Initial buffer size must be positive'), - _size = initialBufferSize, - capacity = initialBufferSize, + : this._validated(_validateInitialBufferSize(initialBufferSize)); + + _WriterState._validated(this._size) + : capacity = _size, offset = 0, - list = Uint8List(initialBufferSize) { + list = Uint8List(_size) { data = list.buffer.asByteData(); } + static int _validateInitialBufferSize(int value) { + if (value <= 0) { + throw RangeError.value( + value, + 'initialBufferSize', + 'Initial buffer size must be positive', + ); + } + return value; + } + /// Current write position in the buffer. late int offset; diff --git a/test/binary_reader_performance_test.dart b/test/binary_reader_performance_test.dart index 414b142..30479b7 100644 --- a/test/binary_reader_performance_test.dart +++ b/test/binary_reader_performance_test.dart @@ -1,7 +1,6 @@ -import 'dart:convert'; - import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; const string = 'Hello, World!'; const longString = @@ -80,10 +79,6 @@ class BinaryReaderBenchmark extends BenchmarkBase { reader.reset(); } } - - static void main() { - BinaryReaderBenchmark().report(); - } } class GetStringLengthBenchmark extends BenchmarkBase { @@ -111,46 +106,14 @@ class GetStringLengthBenchmark extends BenchmarkBase { final _ = getUtf8Length(longString); } } - - static void main() { - GetStringLengthBenchmark().report(); - } -} - -class GetStringLengthUtf8Benchmark extends BenchmarkBase { - GetStringLengthUtf8Benchmark() - : super('GetStringLengthUtf8 performance test'); - - @override - void exercise() => run(); - - @override - void run() { - for (var i = 0; i < 1000; i++) { - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - final _ = utf8.encode(string).length; - final _ = utf8.encode(longString).length; - } - } - - static void main() { - GetStringLengthUtf8Benchmark().report(); - } } void main() { - BinaryReaderBenchmark.main(); - GetStringLengthBenchmark.main(); - GetStringLengthUtf8Benchmark.main(); + test('BinaryReaderBenchmark', () { + BinaryReaderBenchmark().report(); + }, tags: ['benchmark']); + + test('GetStringLengthBenchmark', () { + GetStringLengthBenchmark().report(); + }, tags: ['benchmark']); } diff --git a/test/binary_reader_test.dart b/test/binary_reader_test.dart index 61e45bd..22c0520 100644 --- a/test/binary_reader_test.dart +++ b/test/binary_reader_test.dart @@ -387,7 +387,7 @@ void main() { final buffer = Uint8List.fromList([0x80]); // MSB=1, expects more bytes final reader = BinaryReader(buffer); - expect(reader.readVarUint, throwsA(isA())); + expect(reader.readVarUint, throwsA(isA())); }); test('readVarUint throws on incomplete multi-byte varint', () { @@ -395,7 +395,7 @@ void main() { final buffer = Uint8List.fromList([0xFF]); // All continuation bits set final reader = BinaryReader(buffer); - expect(reader.readVarUint, throwsA(isA())); + expect(reader.readVarUint, throwsA(isA())); }); test('readVarUint throws FormatException on too long varint', () { @@ -415,7 +415,7 @@ void main() { final buffer = Uint8List.fromList([0x80]); final reader = BinaryReader(buffer); - expect(reader.readVarInt, throwsA(isA())); + expect(reader.readVarInt, throwsA(isA())); }); test('readBytes', () { @@ -489,83 +489,83 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('read beyond buffer throws AssertionError', () { + test('read beyond buffer throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(reader.readUint32, throwsA(isA())); + expect(reader.readUint32, throwsA(isA())); }); - test('negative length input throws AssertionError', () { + test('negative length input throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.readBytes(-1), throwsA(isA())); - expect(() => reader.skip(-5), throwsA(isA())); - expect(() => reader.peekBytes(-2), throwsA(isA())); + expect(() => reader.readBytes(-1), throwsA(isA())); + expect(() => reader.skip(-5), throwsA(isA())); + expect(() => reader.peekBytes(-2), throwsA(isA())); }); test('reading from empty buffer', () { final buffer = Uint8List.fromList([]); final reader = BinaryReader(buffer); - expect(reader.readUint8, throwsA(isA())); + expect(reader.readUint8, throwsA(isA())); }); test('reading with offset at end of buffer', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer)..skip(2); - expect(reader.readUint8, throwsA(isA())); + expect(reader.readUint8, throwsA(isA())); }); - test('peekBytes beyond buffer throws AssertionError', () { + test('peekBytes beyond buffer throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.peekBytes(3), throwsA(isA())); - expect(() => reader.peekBytes(1, 2), throwsA(isA())); + expect(() => reader.peekBytes(3), throwsA(isA())); + expect(() => reader.peekBytes(1, 2), throwsA(isA())); }); - test('readString with insufficient bytes throws AssertionError', () { + test('readString with insufficient bytes throws RangeError', () { final buffer = Uint8List.fromList([0x48, 0x65]); // 'He' final reader = BinaryReader(buffer); - expect(() => reader.readString(5), throwsA(isA())); + expect(() => reader.readString(5), throwsA(isA())); }); - test('readBytes with insufficient bytes throws AssertionError', () { + test('readBytes with insufficient bytes throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.readBytes(3), throwsA(isA())); + expect(() => reader.readBytes(3), throwsA(isA())); }); - test('read methods throw AssertionError when not enough bytes', () { + test('read methods throw RangeError when not enough bytes', () { final buffer = Uint8List.fromList([0x00, 0x01]); final reader = BinaryReader(buffer); - expect(reader.readUint32, throwsA(isA())); - expect(reader.readInt32, throwsA(isA())); - expect(reader.readFloat32, throwsA(isA())); + expect(reader.readUint32, throwsA(isA())); + expect(reader.readInt32, throwsA(isA())); + expect(reader.readFloat32, throwsA(isA())); }); test( - 'readUint64 and readInt64 with insufficient bytes throw AssertionError', + 'readUint64 and readInt64 with insufficient bytes throw RangeError', () { final buffer = Uint8List.fromList(List.filled(7, 0x00)); // Only 7 bytes final reader = BinaryReader(buffer); - expect(reader.readUint64, throwsA(isA())); - expect(reader.readInt64, throwsA(isA())); + expect(reader.readUint64, throwsA(isA())); + expect(reader.readInt64, throwsA(isA())); }, ); - test('skip beyond buffer throws AssertionError', () { + test('skip beyond buffer throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); - expect(() => reader.skip(3), throwsA(isA())); + expect(() => reader.skip(3), throwsA(isA())); }); test('read and verify multiple values sequentially', () { @@ -594,42 +594,42 @@ void main() { final buffer = Uint8List.fromList([]); final reader = BinaryReader(buffer); - expect(reader.readUint8, throwsA(isA())); + expect(reader.readUint8, throwsA(isA())); }); test('readInt8 throws when buffer is empty', () { final buffer = Uint8List.fromList([]); final reader = BinaryReader(buffer); - expect(reader.readInt8, throwsA(isA())); + expect(reader.readInt8, throwsA(isA())); }); test('readUint16 throws when only 1 byte available', () { final buffer = Uint8List.fromList([0x01]); final reader = BinaryReader(buffer); - expect(reader.readUint16, throwsA(isA())); + expect(reader.readUint16, throwsA(isA())); }); test('readInt16 throws when only 1 byte available', () { final buffer = Uint8List.fromList([0xFF]); final reader = BinaryReader(buffer); - expect(reader.readInt16, throwsA(isA())); + expect(reader.readInt16, throwsA(isA())); }); test('readUint32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(reader.readUint32, throwsA(isA())); + expect(reader.readUint32, throwsA(isA())); }); test('readInt32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF]); final reader = BinaryReader(buffer); - expect(reader.readInt32, throwsA(isA())); + expect(reader.readInt32, throwsA(isA())); }); test('readUint64 throws when only 7 bytes available', () { @@ -644,7 +644,7 @@ void main() { ]); final reader = BinaryReader(buffer); - expect(reader.readUint64, throwsA(isA())); + expect(reader.readUint64, throwsA(isA())); }); test('readInt64 throws when only 7 bytes available', () { @@ -659,14 +659,14 @@ void main() { ]); final reader = BinaryReader(buffer); - expect(reader.readInt64, throwsA(isA())); + expect(reader.readInt64, throwsA(isA())); }); test('readFloat32 throws when only 3 bytes available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(reader.readFloat32, throwsA(isA())); + expect(reader.readFloat32, throwsA(isA())); }); test('readFloat64 throws when only 7 bytes available', () { @@ -681,28 +681,28 @@ void main() { ]); final reader = BinaryReader(buffer); - expect(reader.readFloat64, throwsA(isA())); + expect(reader.readFloat64, throwsA(isA())); }); test('readBytes throws when requested length exceeds available', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(() => reader.readBytes(5), throwsA(isA())); + expect(() => reader.readBytes(5), throwsA(isA())); }); test('readBytes throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(() => reader.readBytes(-1), throwsA(isA())); + expect(() => reader.readBytes(-1), throwsA(isA())); }); test('readString throws when requested length exceeds available', () { final buffer = Uint8List.fromList([0x48, 0x65, 0x6C]); // "Hel" final reader = BinaryReader(buffer); - expect(() => reader.readString(10), throwsA(isA())); + expect(() => reader.readString(10), throwsA(isA())); }); test('multiple reads exceed buffer size', () { @@ -712,28 +712,28 @@ void main() { ..readUint8() // 1 byte read, 2 remaining ..readUint16(); // 2 bytes read, 0 remaining - expect(reader.readUint8, throwsA(isA())); + expect(reader.readUint8, throwsA(isA())); }); test('peekBytes throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(() => reader.peekBytes(-1), throwsA(isA())); + expect(() => reader.peekBytes(-1), throwsA(isA())); }); test('skip throws when length exceeds available bytes', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(() => reader.skip(5), throwsA(isA())); + expect(() => reader.skip(5), throwsA(isA())); }); test('skip throws when length is negative', () { final buffer = Uint8List.fromList([0x01, 0x02, 0x03]); final reader = BinaryReader(buffer); - expect(() => reader.skip(-1), throwsA(isA())); + expect(() => reader.skip(-1), throwsA(isA())); }); }); @@ -1302,7 +1302,7 @@ void main() { expect( reader.readVarBytes, - throwsA(isA()), + throwsA(isA()), ); }); @@ -1312,7 +1312,7 @@ void main() { expect( reader.readVarBytes, - throwsA(isA()), + throwsA(isA()), ); }); @@ -1387,7 +1387,7 @@ void main() { expect( reader.readVarString, - throwsA(isA()), + throwsA(isA()), ); }); @@ -1397,7 +1397,7 @@ void main() { expect( reader.readVarString, - throwsA(isA()), + throwsA(isA()), ); }); @@ -1529,13 +1529,13 @@ void main() { final buffer = Uint8List.fromList([]); final reader = BinaryReader(buffer); - expect(reader.readBool, throwsA(isA())); + expect(reader.readBool, throwsA(isA())); }); test('throws when no bytes available', () { final buffer = Uint8List.fromList([0x01]); final reader = BinaryReader(buffer)..readBool(); // Consume the byte - expect(reader.readBool, throwsA(isA())); + expect(reader.readBool, throwsA(isA())); }); }); @@ -1759,15 +1759,15 @@ void main() { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer); - expect(() => reader.seek(-1), throwsA(isA())); + expect(() => reader.seek(-1), throwsA(isA())); }); test('throws when seeking beyond buffer', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer); - expect(() => reader.seek(6), throwsA(isA())); - expect(() => reader.seek(100), throwsA(isA())); + expect(() => reader.seek(6), throwsA(isA())); + expect(() => reader.seek(100), throwsA(isA())); }); }); @@ -1849,21 +1849,21 @@ void main() { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer)..readUint16(); // offset = 2 - expect(() => reader.rewind(3), throwsA(isA())); + expect(() => reader.rewind(3), throwsA(isA())); }); test('throws when rewinding from start', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer); - expect(() => reader.rewind(1), throwsA(isA())); + expect(() => reader.rewind(1), throwsA(isA())); }); test('throws on negative length', () { final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); final reader = BinaryReader(buffer)..readBytes(3); - expect(() => reader.rewind(-1), throwsA(isA())); + expect(() => reader.rewind(-1), throwsA(isA())); }); }); }); diff --git a/test/binary_writer_performance_test.dart b/test/binary_writer_performance_test.dart index dee7b29..9c268f0 100644 --- a/test/binary_writer_performance_test.dart +++ b/test/binary_writer_performance_test.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; const longStringWithEmoji = 'The quick brown fox 🦊 jumps over the lazy dog 🐕. ' @@ -98,9 +99,6 @@ class BinaryWriterBenchmark extends BenchmarkBase { @override void exercise() => run(); - static void main() { - BinaryWriterBenchmark().report(); - } } class BinaryWriterVarIntBenchmark extends BenchmarkBase { @@ -148,12 +146,14 @@ class BinaryWriterVarIntBenchmark extends BenchmarkBase { @override void exercise() => run(); - static void main() { - BinaryWriterVarIntBenchmark().report(); - } } void main() { - BinaryWriterBenchmark.main(); - BinaryWriterVarIntBenchmark.main(); + test('BinaryWriterBenchmark', () { + BinaryWriterBenchmark().report(); + }, tags: ['benchmark']); + + test('BinaryWriterVarIntBenchmark', () { + BinaryWriterVarIntBenchmark().report(); + }, tags: ['benchmark']); } diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index e211924..e4e4d0e 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -12,6 +12,15 @@ void main() { writer = BinaryWriter(); }); + test('throw RangeError when initialBufferSize is not positive', () { + expect( + () => BinaryWriter(initialBufferSize: 0), + throwsA( + isA().having((e) => e.name, 'name', 'initialBufferSize'), + ), + ); + }); + test('return empty list when takeBytes called on empty writer', () { expect(writer.takeBytes(), isEmpty); }); @@ -361,7 +370,7 @@ void main() { }); group('Input validation', () { - test('throw AssertionError when Uint8 value is negative', () { + test('throw RangeError when Uint8 value is negative', () { expect( () => writer.writeUint8(-1), throwsA( @@ -373,7 +382,7 @@ void main() { ); }); - test('throw AssertionError when Uint8 value exceeds 255', () { + test('throw RangeError when Uint8 value exceeds 255', () { expect( () => writer.writeUint8(256), throwsA( @@ -385,7 +394,7 @@ void main() { ); }); - test('throw AssertionError when Int8 value is less than -128', () { + test('throw RangeError when Int8 value is less than -128', () { expect( () => writer.writeInt8(-129), throwsA( @@ -397,7 +406,7 @@ void main() { ); }); - test('throw AssertionError when Int8 value exceeds 127', () { + test('throw RangeError when Int8 value exceeds 127', () { expect( () => writer.writeInt8(128), throwsA( @@ -409,7 +418,7 @@ void main() { ); }); - test('throw AssertionError when Uint16 value is negative', () { + test('throw RangeError when Uint16 value is negative', () { expect( () => writer.writeUint16(-1), throwsA( @@ -421,7 +430,7 @@ void main() { ); }); - test('throw AssertionError when Uint16 value exceeds 65535', () { + test('throw RangeError when Uint16 value exceeds 65535', () { expect( () => writer.writeUint16(65536), throwsA( @@ -434,7 +443,7 @@ void main() { }); test( - 'should throw AssertionError when Int16 value is less than -32768', + 'should throw RangeError when Int16 value is less than -32768', () { expect( () => writer.writeInt16(-32769), @@ -448,7 +457,7 @@ void main() { }, ); - test('throw AssertionError when Int16 value exceeds 32767', () { + test('throw RangeError when Int16 value exceeds 32767', () { expect( () => writer.writeInt16(32768), throwsA( @@ -460,7 +469,7 @@ void main() { ); }); - test('throw AssertionError when Uint32 value is negative', () { + test('throw RangeError when Uint32 value is negative', () { expect( () => writer.writeUint32(-1), throwsA( @@ -473,7 +482,7 @@ void main() { }); test( - 'should throw AssertionError when Uint32 value exceeds 4294967295', + 'should throw RangeError when Uint32 value exceeds 4294967295', () { expect( () => writer.writeUint32(4294967296), @@ -488,7 +497,7 @@ void main() { ); test( - 'should throw AssertionError when Int32 value is less than -2147483648', + 'should throw RangeError when Int32 value is less than -2147483648', () { expect( () => writer.writeInt32(-2147483649), @@ -503,7 +512,7 @@ void main() { ); test( - 'should throw AssertionError when Int32 value exceeds 2147483647', + 'should throw RangeError when Int32 value exceeds 2147483647', () { expect( () => writer.writeInt32(2147483648), @@ -666,7 +675,7 @@ void main() { expect(value.isNegative, isTrue); }); - test('throw AssertionError when Uint64 value is negative', () { + test('throw RangeError when Uint64 value is negative', () { expect( () => writer.writeUint64(-1), throwsA( @@ -905,7 +914,7 @@ void main() { final data = [1, 2, 3, 4, 5]; expect( () => writer.writeBytes(data, -1), - throwsA(isA()), + throwsA(isA()), ); }); @@ -913,7 +922,7 @@ void main() { final data = [1, 2, 3, 4, 5]; expect( () => writer.writeBytes(data, 0, -1), - throwsA(isA()), + throwsA(isA()), ); }); @@ -921,7 +930,7 @@ void main() { final data = [1, 2, 3]; expect( () => writer.writeBytes(data, 4), - throwsA(isA()), + throwsA(isA()), ); }); @@ -930,7 +939,7 @@ void main() { expect( // offset 2 + length 5 > list length 5 () => writer.writeBytes(data, 2, 5), - throwsA(isA()), + throwsA(isA()), ); }); }); From 2fd88bac32468d067e7c1a5351d4e1388b345ee6 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 30 Dec 2025 13:45:24 +0200 Subject: [PATCH 16/29] feat: Add BinaryWriterPool with statistics and tests for pool behavior and performance --- lib/src/binary_writer.dart | 18 ++- test/binary_writer_test.dart | 299 +++++++++++++++++++++++++++++++++++ 2 files changed, 315 insertions(+), 2 deletions(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 08d7b73..cc82723 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1020,12 +1020,12 @@ abstract final class BinaryWriterPool { /// final stats = BinaryWriterPool.getStatistics(); /// print('Pooled writers: ${stats['pooled']}'); // 5 /// ``` - static Map getStatistics() => { + static PoolStatistics get stats => PoolStatistics({ 'pooled': _pool.length, 'maxPoolSize': _maxPoolSize, 'defaultBufferSize': _defaultBufferSize, 'maxReusableCapacity': _maxReusableCapacity, - }; + }); /// Clears the pool, releasing all cached writers. /// @@ -1042,3 +1042,17 @@ abstract final class BinaryWriterPool { /// ``` static void clear() => _pool.clear(); } + +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']!; +} diff --git a/test/binary_writer_test.dart b/test/binary_writer_test.dart index e4e4d0e..69d2437 100644 --- a/test/binary_writer_test.dart +++ b/test/binary_writer_test.dart @@ -1885,4 +1885,303 @@ void main() { }); }); }); + + group('BinaryWriterPool', () { + setUp(BinaryWriterPool.clear); + + tearDown(BinaryWriterPool.clear); + + test('acquire returns a working writer', () { + final writer = BinaryWriterPool.acquire()..writeUint32(42); + final bytes = writer.toBytes(); + BinaryWriterPool.release(writer); + + expect(bytes, hasLength(4)); + }); + + test('acquire creates new writer when pool is empty', () { + expect(BinaryWriterPool.stats.pooled, equals(0)); + + final writer = BinaryWriterPool.acquire(); + expect(writer, isNotNull); + BinaryWriterPool.release(writer); + }); + + test('release returns writer to pool', () { + final writer = BinaryWriterPool.acquire() + //Write some data to ensure buffer is used + ..writeUint32(42); + BinaryWriterPool.release(writer); + + final stats = BinaryWriterPool.stats; + expect(stats.pooled, equals(1)); + }); + + test('acquire reuses pooled writer', () { + final writer1 = BinaryWriterPool.acquire() + // Write some data to ensure buffer is used + ..writeUint32(42); + + BinaryWriterPool.release(writer1); + + expect(BinaryWriterPool.stats.pooled, equals(1)); + + final writer2 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.pooled, equals(0)); + + // Writer should be cleared + expect(writer2.bytesWritten, equals(0)); + + BinaryWriterPool.release(writer2); + }); + + test('released writer is reset', () { + final writer = BinaryWriterPool.acquire() + ..writeUint32(42) + ..writeString('Hello'); + BinaryWriterPool.release(writer); + + final reusedWriter = BinaryWriterPool.acquire(); + expect(reusedWriter.bytesWritten, equals(0)); + + reusedWriter.writeUint8(1); + final bytes = reusedWriter.toBytes(); + expect(bytes, equals([1])); + + BinaryWriterPool.release(reusedWriter); + }); + + test('clear empties the pool', () { + final writer1 = BinaryWriterPool.acquire(); + final writer2 = BinaryWriterPool.acquire(); + final writer3 = BinaryWriterPool.acquire(); + + BinaryWriterPool.release(writer1); + BinaryWriterPool.release(writer2); + BinaryWriterPool.release(writer3); + + expect(BinaryWriterPool.stats.pooled, equals(3)); + + BinaryWriterPool.clear(); + expect(BinaryWriterPool.stats.pooled, equals(0)); + }); + + test('getStatistics returns correct information', () { + final stats = BinaryWriterPool.stats; + + expect(stats.pooled, equals(0)); + expect(stats.maxPoolSize, equals(32)); + expect(stats.defaultBufferSize, equals(1024)); + expect(stats.maxReusableCapacity, equals(64 * 1024)); + }); + + test('pool respects max pool size', () { + // Create and release more writers than the pool can hold + final writers = []; + for (var i = 0; i < 40; i++) { + writers.add(BinaryWriterPool.acquire()); + } + + writers.forEach(BinaryWriterPool.release); + + final stats = BinaryWriterPool.stats; + expect(stats.pooled, equals(32)); // Max pool size + }); + + test('writers with large buffers are not pooled', () { + final writer = BinaryWriterPool.acquire(); + + // Write enough data to expand buffer beyond 64 KiB + final largeData = List.filled(70 * 1024, 42); + writer.writeBytes(largeData); + + BinaryWriterPool.release(writer); + + // Writer should not be pooled due to large buffer + final stats = BinaryWriterPool.stats; + expect(stats.pooled, equals(0)); + }); + + test('double release is safe (ignored)', () { + final writer = BinaryWriterPool.acquire(); + BinaryWriterPool.release(writer); + expect(BinaryWriterPool.stats.pooled, equals(1)); + + // Second release should be ignored + BinaryWriterPool.release(writer); + expect(BinaryWriterPool.stats.pooled, equals(1)); + }); + + test('multiple writers work independently', () { + final writer1 = BinaryWriterPool.acquire(); + final writer2 = BinaryWriterPool.acquire(); + final writer3 = BinaryWriterPool.acquire(); + + writer1.writeUint32(100); + writer2.writeUint32(200); + writer3.writeUint32(300); + + final bytes1 = writer1.toBytes(); + final bytes2 = writer2.toBytes(); + final bytes3 = writer3.toBytes(); + + final reader1 = BinaryReader(bytes1); + final reader2 = BinaryReader(bytes2); + final reader3 = BinaryReader(bytes3); + + expect(reader1.readUint32(), equals(100)); + expect(reader2.readUint32(), equals(200)); + expect(reader3.readUint32(), equals(300)); + + BinaryWriterPool.release(writer1); + BinaryWriterPool.release(writer2); + BinaryWriterPool.release(writer3); + }); + + test('try-finally pattern works correctly', () { + late Uint8List bytes; + + final writer = BinaryWriterPool.acquire(); + try { + writer + ..writeUint32(42) + ..writeString('Test'); + bytes = writer.toBytes(); + } finally { + BinaryWriterPool.release(writer); + } + + expect(bytes, isNotNull); + expect(BinaryWriterPool.stats.pooled, equals(1)); + + final reader = BinaryReader(bytes); + expect(reader.readUint32(), equals(42)); + }); + + test('takeBytes and release work together', () { + final writer = BinaryWriterPool.acquire()..writeUint32(123); + final bytes = writer.takeBytes(); // This resets the writer + BinaryWriterPool.release(writer); + + expect(bytes, hasLength(4)); + expect(BinaryWriterPool.stats.pooled, equals(1)); + + // Verify writer was properly reset when returned + final reusedWriter = BinaryWriterPool.acquire(); + expect(reusedWriter.bytesWritten, equals(0)); + BinaryWriterPool.release(reusedWriter); + }); + + test('pool handles multiple acquire-release cycles', () { + for (var cycle = 0; cycle < 10; cycle++) { + final writer = BinaryWriterPool.acquire()..writeUint32(cycle); + + final bytes = writer.toBytes(); + final reader = BinaryReader(bytes); + expect(reader.readUint32(), equals(cycle)); + + BinaryWriterPool.release(writer); + } + + // Should have 1 writer in pool after all cycles + expect(BinaryWriterPool.stats.pooled, equals(1)); + }); + + test('writers can write complex data structures', () { + final writer = BinaryWriterPool.acquire(); + try { + // Write a complex structure + writer + ..writeVarUint(5) // Array length + ..writeString('Item1') + ..writeString('Item2') + ..writeString('Item3') + ..writeString('Привет') // Cyrillic + ..writeString('🌍'); // Emoji + + final bytes = writer.toBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(5)); + expect(reader.readString(5), equals('Item1')); + expect(reader.readString(5), equals('Item2')); + expect(reader.readString(5), equals('Item3')); + expect(reader.readString(12), equals('Привет')); + expect(reader.readString(4), equals('🌍')); + } finally { + BinaryWriterPool.release(writer); + } + }); + + test('pool statistics remain accurate during stress test', () { + // Acquire multiple writers + final writers = []; + for (var i = 0; i < 10; i++) { + writers.add(BinaryWriterPool.acquire()); + } + expect(BinaryWriterPool.stats.pooled, equals(0)); + + // Release half + for (var i = 0; i < 5; i++) { + BinaryWriterPool.release(writers[i]); + } + expect(BinaryWriterPool.stats.pooled, equals(5)); + + // Acquire some back + for (var i = 0; i < 3; i++) { + BinaryWriterPool.acquire(); + } + expect(BinaryWriterPool.stats.pooled, equals(2)); + + // Release remaining + for (var i = 5; i < 10; i++) { + BinaryWriterPool.release(writers[i]); + } + expect(BinaryWriterPool.stats.pooled, equals(7)); + }); + + test('default buffer size is appropriate for common use cases', () { + final writer = BinaryWriterPool.acquire(); + try { + // Write typical message + writer + ..writeUint32(12345) + ..writeString('Username') + ..writeFloat64(3.14159) + ..writeBool(true); + + expect(writer.bytesWritten, lessThan(1024)); // Default buffer size + } finally { + BinaryWriterPool.release(writer); + } + }); + + test('pool handles edge case of zero writes', () { + final writer = BinaryWriterPool.acquire(); + // Don't write anything + final bytes = writer.toBytes(); + BinaryWriterPool.release(writer); + + expect(bytes, isEmpty); + expect(BinaryWriterPool.stats.pooled, equals(1)); + }); + + test('pooled writer buffer capacity persists across reuse', () { + final writer1 = BinaryWriterPool.acquire(); + + // Expand buffer by writing data + final data = List.filled(2048, 42); + writer1.writeBytes(data); + + BinaryWriterPool.release(writer1); + + // Reuse the same writer + final writer2 = BinaryWriterPool.acquire() + // Writing smaller amount should not allocate new buffer + ..writeUint32(123); + expect(writer2.bytesWritten, equals(4)); + + BinaryWriterPool.release(writer2); + }); + }); } From 6478a06e4ea88fc250c3fbfab9db8bb68cdca328 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 30 Dec 2025 13:53:36 +0200 Subject: [PATCH 17/29] fix: Update documentation for BinaryWriterPool to correct reference from getStatistics to stats --- lib/src/binary_writer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index cc82723..6ecab4d 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -913,7 +913,7 @@ int getUtf8Length(String value) { /// - Default buffer size: 1 KiB /// - Use [clear] to free pooled memory explicitly /// -/// See also: [BinaryWriter], [getStatistics] for pool monitoring +/// See also: [BinaryWriter], [stats] for pool monitoring abstract final class BinaryWriterPool { // The internal pool of reusable writer states. static final _pool = <_WriterState>[]; From 6ce0e657b9652715dca3d92561409f072fa33e5f Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 30 Dec 2025 13:55:40 +0200 Subject: [PATCH 18/29] Refactor code structure for improved readability and maintainability --- test/{ => integration}/integration_test.dart | 0 test/{ => performance}/binary_reader_performance_test.dart | 0 test/{ => performance}/binary_writer_performance_test.dart | 0 test/{ => unit}/binary_reader_test.dart | 0 test/{ => unit}/binary_writer_test.dart | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => integration}/integration_test.dart (100%) rename test/{ => performance}/binary_reader_performance_test.dart (100%) rename test/{ => performance}/binary_writer_performance_test.dart (100%) rename test/{ => unit}/binary_reader_test.dart (100%) rename test/{ => unit}/binary_writer_test.dart (100%) diff --git a/test/integration_test.dart b/test/integration/integration_test.dart similarity index 100% rename from test/integration_test.dart rename to test/integration/integration_test.dart diff --git a/test/binary_reader_performance_test.dart b/test/performance/binary_reader_performance_test.dart similarity index 100% rename from test/binary_reader_performance_test.dart rename to test/performance/binary_reader_performance_test.dart diff --git a/test/binary_writer_performance_test.dart b/test/performance/binary_writer_performance_test.dart similarity index 100% rename from test/binary_writer_performance_test.dart rename to test/performance/binary_writer_performance_test.dart diff --git a/test/binary_reader_test.dart b/test/unit/binary_reader_test.dart similarity index 100% rename from test/binary_reader_test.dart rename to test/unit/binary_reader_test.dart diff --git a/test/binary_writer_test.dart b/test/unit/binary_writer_test.dart similarity index 100% rename from test/binary_writer_test.dart rename to test/unit/binary_writer_test.dart From e076b443422403e44258314a99d50bed487402ca Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 30 Dec 2025 15:10:06 +0200 Subject: [PATCH 19/29] Add benchmarks for binary reader operations - Implemented benchmarks for skip operations with small, medium, and large offsets. - Added seek operation benchmarks for forward, backward, and random access. - Created benchmarks for rewind, reset, and getPosition operations. - Included benchmarks for remainingBytes and realistic parsing navigation patterns. - Developed string read benchmarks for ASCII, short, long, Cyrillic, CJK, emoji, and mixed Unicode strings. - Added benchmarks for VarString reading and realistic message structures. - Implemented benchmarks for reading VarInt and VarUint with various sizes and distributions. --- README.md | 8 +- .../micro/reader/binary_read_bench.dart | 465 +++++++++++++++++ .../micro/reader/fixed_int_read_bench.dart | 473 +++++++++++++++++ .../micro/reader/float_read_bench.dart | 422 +++++++++++++++ .../micro/reader/navigation_bench.dart | 482 ++++++++++++++++++ .../micro/reader/string_read_bench.dart | 481 +++++++++++++++++ .../micro/reader/varint_read_bench.dart | 318 ++++++++++++ 7 files changed, 2645 insertions(+), 4 deletions(-) create mode 100644 test/performance/micro/reader/binary_read_bench.dart create mode 100644 test/performance/micro/reader/fixed_int_read_bench.dart create mode 100644 test/performance/micro/reader/float_read_bench.dart create mode 100644 test/performance/micro/reader/navigation_bench.dart create mode 100644 test/performance/micro/reader/string_read_bench.dart create mode 100644 test/performance/micro/reader/varint_read_bench.dart diff --git a/README.md b/README.md index 9005de6..08eba0b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ final writer = BinaryWriter(); // Integers (8, 16, 32, 64-bit signed/unsigned) writer.writeUint8(255); -writer.writeInt32(-1000, Endian.little); +writer.writeInt32(-1000, .little); writer.writeUint64(9999999); // Floats @@ -75,7 +75,7 @@ final reader = BinaryReader(bytes); // Read primitives (matching write order) final u8 = reader.readUint8(); -final i32 = reader.readInt32(Endian.little); +final i32 = reader.readInt32(.little); final f64 = reader.readFloat64(); // Variable-length integers @@ -199,10 +199,10 @@ VarInt encoding significantly reduces payload size for small values: final writer = BinaryWriter(initialBufferSize: 1024); ``` -**Endianness**: Defaults to big-endian. Specify when needed: +**Endianness**: Defaults to big-. Specify when needed: ```dart -writer.writeUint32(value, Endian.little); +writer.writeUint32(value, .little); ``` **String Encoding**: Always use length-prefix for variable strings: diff --git a/test/performance/micro/reader/binary_read_bench.dart b/test/performance/micro/reader/binary_read_bench.dart new file mode 100644 index 0000000..43f6dfd --- /dev/null +++ b/test/performance/micro/reader/binary_read_bench.dart @@ -0,0 +1,465 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading small byte arrays (< 16 bytes) +/// +/// Small reads are common for fixed-size headers, checksums, and IDs. +class SmallBytesReadBenchmark extends BenchmarkBase { + SmallBytesReadBenchmark() : super('Bytes read: small (8 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + + // Write 1000 small byte arrays + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(8); + } + reader.reset(); + } +} + +/// Benchmark for reading medium byte arrays (64 bytes) +class MediumBytesReadBenchmark extends BenchmarkBase { + MediumBytesReadBenchmark() : super('Bytes read: medium (64 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + final data = Uint8List.fromList(List.generate(64, (i) => i % 256)); + + // Write 1000 medium byte arrays + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(64); + } + reader.reset(); + } +} + +/// Benchmark for reading large byte arrays (1 KB) +class LargeBytesReadBenchmark extends BenchmarkBase { + LargeBytesReadBenchmark() : super('Bytes read: large (1 KB)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 1024 * 1024); + final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + + // Write 100 large byte arrays + for (var i = 0; i < 100; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readBytes(1024); + } + reader.reset(); + } +} + +/// Benchmark for reading very large byte arrays (64 KB) +class VeryLargeBytesReadBenchmark extends BenchmarkBase { + VeryLargeBytesReadBenchmark() : super('Bytes read: very large (64 KB)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 64 * 1024 * 10); + final data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); + + // Write 10 very large byte arrays + for (var i = 0; i < 10; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 10; i++) { + reader.readBytes(64 * 1024); + } + reader.reset(); + } +} + +/// Benchmark for reading VarBytes (length-prefixed byte arrays) +class VarBytesSmallReadBenchmark extends BenchmarkBase { + VarBytesSmallReadBenchmark() : super('VarBytes read: small'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + + // Write 1000 VarBytes + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarBytes(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarBytes with medium-sized data +class VarBytesMediumReadBenchmark extends BenchmarkBase { + VarBytesMediumReadBenchmark() : super('VarBytes read: medium'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 256 * 1024); + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + + // Write 500 VarBytes + for (var i = 0; i < 500; i++) { + writer.writeVarBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + reader.readVarBytes(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarBytes with large data +class VarBytesLargeReadBenchmark extends BenchmarkBase { + VarBytesLargeReadBenchmark() : super('VarBytes read: large'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 512 * 1024); + final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); + + // Write 100 VarBytes + for (var i = 0; i < 100; i++) { + writer.writeVarBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readVarBytes(); + } + reader.reset(); + } +} + +/// Benchmark for reading empty byte arrays +class EmptyBytesReadBenchmark extends BenchmarkBase { + EmptyBytesReadBenchmark() : super('Bytes read: empty'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + + // Write 1000 empty byte arrays + for (var i = 0; i < 1000; i++) { + writer.writeBytes([]); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readBytes(0); + } + reader.reset(); + } +} + +/// Benchmark for peeking at bytes without advancing position +class PeekBytesReadBenchmark extends BenchmarkBase { + PeekBytesReadBenchmark() : super('Bytes peek: 16 bytes'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + final data = Uint8List.fromList(List.generate(16, (i) => i)); + + writer.writeBytes(data); + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.peekBytes(16); + } + // No reset needed - we're not advancing position + } +} + +/// Benchmark for reading remaining bytes +class ReadRemainingBytesReadBenchmark extends BenchmarkBase { + ReadRemainingBytesReadBenchmark() : super('readRemainingBytes'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + + // Write 100 chunks + for (var i = 0; i < 100; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readBytes(1024); + } + reader.reset(); + } +} + +/// Benchmark for mixed-size byte reads (realistic scenario) +/// +/// Simulates reading a protocol with headers, payloads, and checksums. +class MixedBytesReadBenchmark extends BenchmarkBase { + MixedBytesReadBenchmark() : super('Bytes read: mixed sizes (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + + // Simulate a protocol message: + // - Header (16 bytes) + // - Payload (variable: 64, 128, 256 bytes) + // - Checksum (4 bytes) + for (var i = 0; i < 100; i++) { + final header = Uint8List.fromList(List.generate(16, (j) => j)); + final payload = Uint8List.fromList( + List.generate(64 + (i % 3) * 64, (j) => (j + i) % 256), + ); + final checksum = Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF]); + + writer + ..writeBytes(header) + ..writeBytes(payload) + ..writeBytes(checksum); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader + ..readBytes(16) // Header + ..readBytes(64 + (i % 3) * 64) // Payload + ..readBytes(4); // Checksum + } + reader.reset(); + } +} + +/// Benchmark for alternating small and large reads +class AlternatingBytesReadBenchmark extends BenchmarkBase { + AlternatingBytesReadBenchmark() : super('Bytes read: alternating sizes'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + final small = Uint8List.fromList([1, 2, 3, 4]); + final large = Uint8List.fromList(List.generate(512, (i) => i % 256)); + + // Alternate between small and large + for (var i = 0; i < 100; i++) { + writer + ..writeBytes(small) + ..writeBytes(large); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader + ..readBytes(4) + ..readBytes(512); + } + reader.reset(); + } +} + +/// Benchmark for sequential small reads +/// +/// Tests performance when reading many small chunks sequentially. +class SequentialSmallReadsReadBenchmark extends BenchmarkBase { + SequentialSmallReadsReadBenchmark() + : super('Bytes read: sequential small reads'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + + // Write 4000 bytes as 1-byte chunks + for (var i = 0; i < 4000; i++) { + writer.writeUint8(i % 256); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 4000; i++) { + reader.readBytes(1); + } + reader.reset(); + } +} + +/// Benchmark for reading with skip operations +class SkipAndReadBenchmark extends BenchmarkBase { + SkipAndReadBenchmark() : super('Bytes read: skip + read pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + + // Write pattern: 8 bytes data, 8 bytes padding + for (var i = 0; i < 1000; i++) { + final data = Uint8List.fromList(List.generate(8, (j) => (i + j) % 256)); + final padding = Uint8List.fromList(List.generate(8, (_) => 0)); + writer + ..writeBytes(data) + ..writeBytes(padding); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readBytes(8) // Read data + ..skip(8); // Skip padding + } + reader.reset(); + } +} + +void main() { + test('Fixed-size reads benchmarks:', () { + EmptyBytesReadBenchmark().report(); + SmallBytesReadBenchmark().report(); + MediumBytesReadBenchmark().report(); + LargeBytesReadBenchmark().report(); + VeryLargeBytesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('VarBytes (length-prefixed) benchmarks:', () { + VarBytesSmallReadBenchmark().report(); + VarBytesMediumReadBenchmark().report(); + VarBytesLargeReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Special operations benchmarks:', () { + PeekBytesReadBenchmark().report(); + ReadRemainingBytesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios benchmarks:', () { + MixedBytesReadBenchmark().report(); + AlternatingBytesReadBenchmark().report(); + SequentialSmallReadsReadBenchmark().report(); + SkipAndReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/micro/reader/fixed_int_read_bench.dart b/test/performance/micro/reader/fixed_int_read_bench.dart new file mode 100644 index 0000000..0848d44 --- /dev/null +++ b/test/performance/micro/reader/fixed_int_read_bench.dart @@ -0,0 +1,473 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading Uint8 (1 byte unsigned) +/// +/// Most basic read operation - single byte access without endianness concerns. +/// Should be the fastest fixed-int read operation. +class Uint8ReadBenchmark extends BenchmarkBase { + Uint8ReadBenchmark() : super('Uint8 read'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint8 values + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint8(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int8 (1 byte signed) +class Int8ReadBenchmark extends BenchmarkBase { + Int8ReadBenchmark() : super('Int8 read'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int8 values + for (var i = 0; i < 1000; i++) { + writer.writeInt8((i % 256) - 128); // Range: -128 to 127 + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt8(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint16 in big-endian format +class Uint16BigEndianReadBenchmark extends BenchmarkBase { + Uint16BigEndianReadBenchmark() : super('Uint16 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint16 values + for (var i = 0; i < 1000; i++) { + writer.writeUint16((i * 257) % 65536); // Varied values + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint16(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint16 in little-endian format +class Uint16LittleEndianReadBenchmark extends BenchmarkBase { + Uint16LittleEndianReadBenchmark() : super('Uint16 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint16 values in little-endian + for (var i = 0; i < 1000; i++) { + writer.writeUint16((i * 257) % 65536, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint16(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Int16 in big-endian format +class Int16BigEndianReadBenchmark extends BenchmarkBase { + Int16BigEndianReadBenchmark() : super('Int16 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int16 values + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i * 257) % 65536 - 32768); // Range: -32768 to 32767 + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt16(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int16 in little-endian format +class Int16LittleEndianReadBenchmark extends BenchmarkBase { + Int16LittleEndianReadBenchmark() : super('Int16 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int16 values in little-endian + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i * 257) % 65536 - 32768, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt16(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint32 in big-endian format +class Uint32BigEndianReadBenchmark extends BenchmarkBase { + Uint32BigEndianReadBenchmark() : super('Uint32 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint32 values + for (var i = 0; i < 1000; i++) { + writer.writeUint32((i * 1000000 + i * 123) % 4294967296); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint32(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint32 in little-endian format +class Uint32LittleEndianReadBenchmark extends BenchmarkBase { + Uint32LittleEndianReadBenchmark() : super('Uint32 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint32 values in little-endian + for (var i = 0; i < 1000; i++) { + writer.writeUint32((i * 1000000 + i * 123) % 4294967296, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Int32 in big-endian format +class Int32BigEndianReadBenchmark extends BenchmarkBase { + Int32BigEndianReadBenchmark() : super('Int32 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int32 values + for (var i = 0; i < 1000; i++) { + writer.writeInt32((i * 1000000 + i * 123) % 4294967296 - 2147483648); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt32(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int32 in little-endian format +class Int32LittleEndianReadBenchmark extends BenchmarkBase { + Int32LittleEndianReadBenchmark() : super('Int32 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int32 values in little-endian + for (var i = 0; i < 1000; i++) { + writer.writeInt32( + (i * 1000000 + i * 123) % 4294967296 - 2147483648, + .little, + ); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint64 in big-endian format +class Uint64BigEndianReadBenchmark extends BenchmarkBase { + Uint64BigEndianReadBenchmark() : super('Uint64 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint64 values + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000000 + i * 12345); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint64(); + } + reader.reset(); + } +} + +/// Benchmark for reading Uint64 in little-endian format +class Uint64LittleEndianReadBenchmark extends BenchmarkBase { + Uint64LittleEndianReadBenchmark() : super('Uint64 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Uint64 values in little-endian + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000000 + i * 12345, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readUint64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Int64 in big-endian format +class Int64BigEndianReadBenchmark extends BenchmarkBase { + Int64BigEndianReadBenchmark() : super('Int64 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int64 values + for (var i = 0; i < 1000; i++) { + final value = i.isEven + ? (i * 1000000000 + i * 12345) + : -(i * 1000000000 + i * 12345); + writer.writeInt64(value); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt64(); + } + reader.reset(); + } +} + +/// Benchmark for reading Int64 in little-endian format +class Int64LittleEndianReadBenchmark extends BenchmarkBase { + Int64LittleEndianReadBenchmark() : super('Int64 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Int64 values in little-endian + for (var i = 0; i < 1000; i++) { + final value = i.isEven + ? (i * 1000000000 + i * 12345) + : -(i * 1000000000 + i * 12345); + writer.writeInt64(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readInt64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading mixed fixed-width integers (realistic scenario) +/// +/// Simulates real-world protocol where various integer sizes are mixed. +/// Uses little-endian as it's more common in modern protocols. +class MixedFixedIntReadBenchmark extends BenchmarkBase { + MixedFixedIntReadBenchmark() : super('Mixed fixed-int read (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write mixed integer types as they might appear in a real protocol + for (var i = 0; i < 1000; i++) { + writer + ..writeUint8(127) // Message type + ..writeUint16(10, .little) // Length + ..writeUint32(1000, .little) // ID + ..writeInt32(-100, .little) // Signed value + ..writeUint64(1000000000, .little) // Timestamp + ..writeInt8(64) // Small signed value + ..writeInt16(-1000, .little) // Medium signed value + ..writeInt64(-10000000, .little); // Large signed value + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..readUint8() + ..readUint16(.little) + ..readUint32(.little) + ..readInt32(.little) + ..readUint64(.little) + ..readInt8() + ..readInt16(.little) + ..readInt64(.little); + } + reader.reset(); + } +} + +void main() { + test('8-bit integer benchmarks:', () { + Uint8ReadBenchmark().report(); + Int8ReadBenchmark().report(); + }, tags: ['benchmark']); + + test('16-bit integer benchmarks:', () { + Uint16BigEndianReadBenchmark().report(); + Uint16LittleEndianReadBenchmark().report(); + Int16BigEndianReadBenchmark().report(); + Int16LittleEndianReadBenchmark().report(); + }, tags: ['benchmark']); + + test('32-bit integer benchmarks:', () { + Uint32BigEndianReadBenchmark().report(); + Uint32LittleEndianReadBenchmark().report(); + Int32BigEndianReadBenchmark().report(); + Int32LittleEndianReadBenchmark().report(); + }, tags: ['benchmark']); + + test('64-bit integer benchmarks:', () { + Uint64BigEndianReadBenchmark().report(); + Uint64LittleEndianReadBenchmark().report(); + Int64BigEndianReadBenchmark().report(); + Int64LittleEndianReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed integer benchmarks:', () { + MixedFixedIntReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/micro/reader/float_read_bench.dart b/test/performance/micro/reader/float_read_bench.dart new file mode 100644 index 0000000..c2b9e94 --- /dev/null +++ b/test/performance/micro/reader/float_read_bench.dart @@ -0,0 +1,422 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading Float32 in big-endian format +/// +/// Float32 (IEEE 754 single precision) is commonly used for graphics, +/// game data, and scientific computing where memory efficiency matters. +class Float32BigEndianReadBenchmark extends BenchmarkBase { + Float32BigEndianReadBenchmark() : super('Float32 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Float32 values with varied magnitudes + for (var i = 0; i < 1000; i++) { + final value = (i * 3.14159) - 500.0; + writer.writeFloat32(value); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 in little-endian format +class Float32LittleEndianReadBenchmark extends BenchmarkBase { + Float32LittleEndianReadBenchmark() : super('Float32 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Float32 values in little-endian + for (var i = 0; i < 1000; i++) { + final value = (i * 3.14159) - 500.0; + writer.writeFloat32(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 in big-endian format +/// +/// Float64 (IEEE 754 double precision) is the default floating-point type +/// in Dart and most high-level languages. Used for general-purpose math. +class Float64BigEndianReadBenchmark extends BenchmarkBase { + Float64BigEndianReadBenchmark() : super('Float64 read (big-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Float64 values with varied magnitudes + for (var i = 0; i < 1000; i++) { + final value = (i * 2.718281828) - 1000.0; + writer.writeFloat64(value); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 in little-endian format +class Float64LittleEndianReadBenchmark extends BenchmarkBase { + Float64LittleEndianReadBenchmark() : super('Float64 read (little-endian)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 Float64 values in little-endian + for (var i = 0; i < 1000; i++) { + final value = (i * 2.718281828) - 1000.0; + writer.writeFloat64(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 special values (NaN, Infinity) +/// +/// Special IEEE 754 values may have different performance characteristics +/// due to how hardware handles them. +class Float32SpecialValuesReadBenchmark extends BenchmarkBase { + Float32SpecialValuesReadBenchmark() : super('Float32 read (special values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write special values: NaN, Infinity, -Infinity, -0.0, normal values + for (var i = 0; i < 200; i++) { + writer + ..writeFloat32(double.nan, .little) + ..writeFloat32(double.infinity, .little) + ..writeFloat32(double.negativeInfinity, .little) + ..writeFloat32(-0, .little) + ..writeFloat32(1, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 special values (NaN, Infinity) +class Float64SpecialValuesReadBenchmark extends BenchmarkBase { + Float64SpecialValuesReadBenchmark() : super('Float64 read (special values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write special values: NaN, Infinity, -Infinity, -0.0, normal values + for (var i = 0; i < 200; i++) { + writer + ..writeFloat64(double.nan, .little) + ..writeFloat64(double.infinity, .little) + ..writeFloat64(double.negativeInfinity, .little) + ..writeFloat64(-0, .little) + ..writeFloat64(1, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 with small values (subnormal range) +/// +/// Subnormal numbers (very close to zero) may have different performance. +class Float32SmallValuesReadBenchmark extends BenchmarkBase { + Float32SmallValuesReadBenchmark() : super('Float32 read (small values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write very small values near the subnormal range + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e-38; // Near Float32 min positive normal + writer.writeFloat32(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 with small values (subnormal range) +class Float64SmallValuesReadBenchmark extends BenchmarkBase { + Float64SmallValuesReadBenchmark() : super('Float64 read (small values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write very small values near the subnormal range + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e-308; // Near Float64 min positive normal + writer.writeFloat64(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float32 with large values +class Float32LargeValuesReadBenchmark extends BenchmarkBase { + Float32LargeValuesReadBenchmark() : super('Float32 read (large values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write large values near Float32 max + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e35; // Near Float32 max (~3.4e38) + writer.writeFloat32(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading Float64 with large values +class Float64LargeValuesReadBenchmark extends BenchmarkBase { + Float64LargeValuesReadBenchmark() : super('Float64 read (large values)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write large values near Float64 max + for (var i = 0; i < 1000; i++) { + final value = (i + 1) * 1e305; // Near Float64 max (~1.8e308) + writer.writeFloat64(value, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readFloat64(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading mixed Float32 and Float64 (realistic scenario) +/// +/// Simulates real-world usage where both precision levels are used. +/// For example: positions (Float32) + precise calculations (Float64). +class MixedFloatReadBenchmark extends BenchmarkBase { + MixedFloatReadBenchmark() : super('Mixed float read (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write mixed Float32/Float64 as in a typical game or graphics protocol + for (var i = 0; i < 100; i++) { + writer + // 3D position (Float32 x3) + ..writeFloat32(i * 1.5, .little) + ..writeFloat32(i * 2.0, .little) + ..writeFloat32(i * 0.5, .little) + // Rotation quaternion (Float32 x4) + ..writeFloat32(0.707, .little) + ..writeFloat32(0, .little) + ..writeFloat32(0.707, .little) + ..writeFloat32(0, .little) + // Precise timestamp (Float64) + ..writeFloat64(i * 1000000.0, .little) + // Color (Float32 x4 - RGBA) + ..writeFloat32(0.5, .little) + ..writeFloat32(0.8, .little) + ..writeFloat32(0.2, .little) + ..writeFloat32(1, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + // Read position + reader + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + // Read rotation + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + // Read timestamp + ..readFloat64(.little) + // Read color + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little) + ..readFloat32(.little); + } + reader.reset(); + } +} + +/// Benchmark for reading alternating Float32/Float64 +/// +/// Tests performance when switching between 32-bit and 64-bit reads. +class AlternatingFloatReadBenchmark extends BenchmarkBase { + AlternatingFloatReadBenchmark() : super('Alternating Float32/Float64 read'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Alternate between Float32 and Float64 + for (var i = 0; i < 500; i++) { + writer + ..writeFloat32(i * 3.14, .little) + ..writeFloat64(i * 2.718, .little); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + reader + ..readFloat32(.little) + ..readFloat64(.little); + } + reader.reset(); + } +} + +void main() { + test('Float32 benchmarks:', () { + Float32BigEndianReadBenchmark().report(); + Float32LittleEndianReadBenchmark().report(); + Float32SmallValuesReadBenchmark().report(); + Float32LargeValuesReadBenchmark().report(); + Float32SpecialValuesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Float64 benchmarks:', () { + Float64BigEndianReadBenchmark().report(); + Float64LittleEndianReadBenchmark().report(); + Float64SmallValuesReadBenchmark().report(); + Float64LargeValuesReadBenchmark().report(); + Float64SpecialValuesReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed float benchmarks:', () { + MixedFloatReadBenchmark().report(); + AlternatingFloatReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/micro/reader/navigation_bench.dart b/test/performance/micro/reader/navigation_bench.dart new file mode 100644 index 0000000..2668af0 --- /dev/null +++ b/test/performance/micro/reader/navigation_bench.dart @@ -0,0 +1,482 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for skip operations (small offsets) +/// +/// Skip is commonly used to jump over padding, unused fields, or known +/// sections. +class SkipSmallOffsetBenchmark extends BenchmarkBase { + SkipSmallOffsetBenchmark() : super('Skip: small offset (8 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + // Write 1000 chunks of 8 bytes each + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(8); + } + reader.reset(); + } +} + +/// Benchmark for skip operations (medium offsets) +class SkipMediumOffsetBenchmark extends BenchmarkBase { + SkipMediumOffsetBenchmark() : super('Skip: medium offset (256 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 256 * 1024); + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + // Write 1000 chunks of 256 bytes + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(256); + } + reader.reset(); + } +} + +/// Benchmark for skip operations (large offsets) +class SkipLargeOffsetBenchmark extends BenchmarkBase { + SkipLargeOffsetBenchmark() : super('Skip: large offset (4 KB)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 4 * 1024 * 1024); + final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); + // Write 1000 chunks of 4KB + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(4096); + } + reader.reset(); + } +} + +/// Benchmark for seek operations (forward) +/// +/// Seek is used for random access patterns, like jumping to specific offsets. +class SeekForwardBenchmark extends BenchmarkBase { + SeekForwardBenchmark() : super('Seek: forward (sequential positions)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 100000); + // Write 100KB of data + final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + writer.writeBytes(data); + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + // Seek to 1000 different positions + for (var i = 0; i < 1000; i++) { + reader.seek((i * 100) % 90000); + } + reader.reset(); + } +} + +/// Benchmark for seek operations (backward) +class SeekBackwardBenchmark extends BenchmarkBase { + SeekBackwardBenchmark() : super('Seek: backward (reverse positions)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 100000); + final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + writer.writeBytes(data); + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + reader.seek(90000); // Start near end + } + + @override + void run() { + // Seek backward to 1000 different positions + for (var i = 1000; i > 0; i--) { + reader.seek((i * 90) % 90000); + } + reader.reset(); + } +} + +/// Benchmark for seek operations (random access) +class SeekRandomAccessBenchmark extends BenchmarkBase { + SeekRandomAccessBenchmark() : super('Seek: random access pattern'); + + late BinaryReader reader; + late Uint8List buffer; + late List positions; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 100000); + final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + writer.writeBytes(data); + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + + // Pre-calculate random-like positions (deterministic for consistency) + positions = List.generate(1000, (i) => (i * 7919) % 90000); + } + + @override + void run() { + // Disable lint for using for-in to emphasize the benchmark nature + // ignore: prefer_foreach + for (final pos in positions) { + reader.seek(pos); + } + reader.reset(); + } +} + +/// Benchmark for rewind operations +/// +/// Rewind resets position to the beginning - common in parsing retry scenarios. +class RewindBenchmark extends BenchmarkBase { + RewindBenchmark() : super('Rewind: reset to start'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..skip(8) + ..reset(); + } + } +} + +/// Benchmark for reset operations +/// +/// Reset is similar to rewind - tests the efficiency of position reset. +class ResetBenchmark extends BenchmarkBase { + ResetBenchmark() : super('Reset: position reset'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..skip(8) + ..reset(); + } + } +} + +/// Benchmark for getPosition operations +/// +/// Getting current position (offset) is often needed in parsing to track +/// offsets. +class GetPositionBenchmark extends BenchmarkBase { + GetPositionBenchmark() : super('offset: query current position'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(8); + } + reader.reset(); + } +} + +/// Benchmark for remainingBytes getter +class RemainingBytesBenchmark extends BenchmarkBase { + RemainingBytesBenchmark() : super('availableBytes: query remaining length'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.skip(8); + } + reader.reset(); + } +} + +/// Benchmark for combined navigation operations (realistic parsing) +/// +/// Simulates a parser that needs to: +/// 1. Check position +/// 2. Peek at header +/// 3. Decide to skip or read +/// 4. Move to next section +class RealisticParsingNavigationBenchmark extends BenchmarkBase { + RealisticParsingNavigationBenchmark() + : super('Navigation: realistic parsing pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 32768); + // Write protocol-like data: header (4 bytes) + payload (variable) + for (var i = 0; i < 500; i++) { + final payloadSize = 16 + (i % 8) * 8; + writer + ..writeUint32(payloadSize) // Header with payload size + ..writeBytes(List.generate(payloadSize, (j) => (i + j) % 256)); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + // 1. Get current position + reader.offset; + // 2. Peek at header to determine payload size + final peekData = reader.peekBytes(4); + final payloadSize = ByteData.view(peekData.buffer).getUint32(0); + // 3. Skip header + reader.skip(4); + // 4. Decide: skip payload based on some condition + if (i % 3 == 0) { + reader.skip(payloadSize); + } else { + // Read and process payload + reader.readBytes(payloadSize); + } + } + reader.reset(); + } +} + +/// Benchmark for seek + read pattern +/// +/// Common in binary file formats with indexes or tables of contents. +class SeekAndReadBenchmark extends BenchmarkBase { + SeekAndReadBenchmark() : super('Navigation: seek + read pattern'); + + late BinaryReader reader; + late Uint8List buffer; + late List offsets; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + // Write 100 records of 64 bytes each + offsets = []; + for (var i = 0; i < 100; i++) { + offsets.add(i * 64); // Track offsets manually + final data = Uint8List.fromList(List.generate(64, (j) => (i + j) % 256)); + writer.writeBytes(data); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + // Read records in non-sequential order + for (var i = 0; i < 100; i++) { + final idx = (i * 7) % 100; + reader + ..seek(offsets[idx]) + ..readBytes(64); + } + reader.reset(); + } +} + +/// Benchmark for skip + peek pattern +/// +/// Used when scanning through data looking for specific patterns. +class SkipAndPeekBenchmark extends BenchmarkBase { + SkipAndPeekBenchmark() : super('Navigation: skip + peek pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + // Write pattern: 4 bytes to skip, 4 bytes to peek + for (var i = 0; i < 1000; i++) { + writer + ..writeUint32(0xDEADBEEF) // Skip this + ..writeUint32(i); // Peek at this + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader + ..skip(4) + ..peekBytes(4) + ..skip(4); + } + reader.reset(); + } +} + +/// Benchmark for backward navigation (seek back and re-read) +/// +/// Used when parser needs to backtrack. +class BacktrackNavigationBenchmark extends BenchmarkBase { + BacktrackNavigationBenchmark() : super('Navigation: backtrack pattern'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + for (var i = 0; i < 2000; i++) { + writer.writeUint32(i); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + // Read forward + reader + ..readUint32() + ..readUint32(); + final pos = reader.offset; + reader + ..readUint32() + // Backtrack to saved position + ..seek(pos) + // Re-read + ..readUint32(); + } + reader.reset(); + } +} + +void main() { + test('Skip operation benchmarks:', () { + SkipSmallOffsetBenchmark().report(); + SkipMediumOffsetBenchmark().report(); + SkipLargeOffsetBenchmark().report(); + }, tags: ['benchmark']); + + test('Seek operation benchmarks:', () { + SeekForwardBenchmark().report(); + SeekBackwardBenchmark().report(); + SeekRandomAccessBenchmark().report(); + }, tags: ['benchmark']); + + test('Position control benchmarks:', () { + RewindBenchmark().report(); + ResetBenchmark().report(); + GetPositionBenchmark().report(); + }, tags: ['benchmark']); + + test('Position query benchmarks:', () { + GetPositionBenchmark().report(); + RemainingBytesBenchmark().report(); + }, tags: ['benchmark']); + + test('Complex navigation patterns:', () { + RealisticParsingNavigationBenchmark().report(); + SeekAndReadBenchmark().report(); + SkipAndPeekBenchmark().report(); + BacktrackNavigationBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/micro/reader/string_read_bench.dart b/test/performance/micro/reader/string_read_bench.dart new file mode 100644 index 0000000..063c69c --- /dev/null +++ b/test/performance/micro/reader/string_read_bench.dart @@ -0,0 +1,481 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading ASCII strings (fast path) +/// +/// ASCII-only strings use the fast path in UTF-8 decoding, +/// processing multiple bytes at once. This is the most common case. +class AsciiStringReadBenchmark extends BenchmarkBase { + AsciiStringReadBenchmark() : super('String read: ASCII only'); + + late BinaryReader reader; + late Uint8List buffer; + late int stringLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const asciiString = 'Hello, World! This is a test string 123456789'; + stringLength = asciiString.length; + + // Write 100 ASCII strings + for (var i = 0; i < 100; i++) { + writer.writeString(asciiString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readString(stringLength); + } + reader.reset(); + } +} + +/// Benchmark for reading short ASCII strings (< 16 chars) +class ShortAsciiStringReadBenchmark extends BenchmarkBase { + ShortAsciiStringReadBenchmark() : super('String read: short ASCII'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const strings = [ + 'Hi', + 'Test', + 'Hello', + 'OK', + 'Error', + 'Success', + '123', + 'ABC', + ]; + + // Write 1000 short strings + for (var i = 0; i < 125; i++) { + strings.forEach(writer.writeString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + // Read in same pattern + for (var i = 0; i < 125; i++) { + reader + ..readString(2) // Hi + ..readString(4) // Test + ..readString(5) // Hello + ..readString(2) // OK + ..readString(5) // Error + ..readString(7) // Success + ..readString(3) // 123 + ..readString(3); // ABC + } + reader.reset(); + } +} + +/// Benchmark for reading long ASCII strings (> 100 chars) +class LongAsciiStringReadBenchmark extends BenchmarkBase { + LongAsciiStringReadBenchmark() : super('String read: long ASCII'); + + late BinaryReader reader; + late Uint8List buffer; + late int stringLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 32768); + const longString = + 'The quick brown fox jumps over the lazy dog. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.'; + stringLength = longString.length; + + // Write 100 long ASCII strings + for (var i = 0; i < 100; i++) { + writer.writeString(longString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readString(stringLength); + } + reader.reset(); + } +} + +/// Benchmark for reading Cyrillic strings (2-byte UTF-8) +class CyrillicStringReadBenchmark extends BenchmarkBase { + CyrillicStringReadBenchmark() : super('String read: Cyrillic (2-byte UTF-8)'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const cyrillicString = 'Привет мир! Это тестовая строка на русском языке.'; + byteLength = getUtf8Length(cyrillicString); + + // Write 100 Cyrillic strings + for (var i = 0; i < 100; i++) { + writer.writeString(cyrillicString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading CJK strings (3-byte UTF-8) +class CjkStringReadBenchmark extends BenchmarkBase { + CjkStringReadBenchmark() : super('String read: CJK (3-byte UTF-8)'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const cjkString = '你好世界!这是一个测试字符串。日本語のテストも含まれています。'; + byteLength = getUtf8Length(cjkString); + + // Write 100 CJK strings + for (var i = 0; i < 100; i++) { + writer.writeString(cjkString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading emoji strings (4-byte UTF-8) +class EmojiStringReadBenchmark extends BenchmarkBase { + EmojiStringReadBenchmark() : super('String read: Emoji (4-byte UTF-8)'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const emojiString = '🚀 🌍 🎉 👍 💻 🔥 ⚡ 🎯 🏆 💡 🌈 ✨ 🎨 🎭 🎪'; + byteLength = getUtf8Length(emojiString); + + // Write 100 emoji strings + for (var i = 0; i < 100; i++) { + writer.writeString(emojiString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading mixed Unicode strings +/// +/// Real-world strings often contain a mix of ASCII, Latin Extended, +/// Cyrillic, CJK, and emoji characters. +class MixedUnicodeStringReadBenchmark extends BenchmarkBase { + MixedUnicodeStringReadBenchmark() : super('String read: mixed Unicode'); + + late BinaryReader reader; + late Uint8List buffer; + late int byteLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const mixedString = 'Hello мир 世界 🌍! Test тест 测试 🚀'; + byteLength = getUtf8Length(mixedString); + + // Write 100 mixed strings + for (var i = 0; i < 100; i++) { + writer.writeString(mixedString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readString(byteLength); + } + reader.reset(); + } +} + +/// Benchmark for reading VarString (length-prefixed strings) +class VarStringAsciiReadBenchmark extends BenchmarkBase { + VarStringAsciiReadBenchmark() : super('VarString read: ASCII'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const asciiString = 'Hello, World! This is a test string.'; + + // Write 100 VarStrings + for (var i = 0; i < 100; i++) { + writer.writeVarString(asciiString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readVarString(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarString with mixed Unicode +class VarStringMixedReadBenchmark extends BenchmarkBase { + VarStringMixedReadBenchmark() : super('VarString read: mixed Unicode'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 16384); + const mixedString = 'Hello мир 世界 🌍 Test тест 测试 🚀'; + + // Write 100 VarStrings + for (var i = 0; i < 100; i++) { + writer.writeVarString(mixedString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + reader.readVarString(); + } + reader.reset(); + } +} + +/// Benchmark for reading empty strings +class EmptyStringReadBenchmark extends BenchmarkBase { + EmptyStringReadBenchmark() : super('String read: empty strings'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + + // Write 1000 empty strings + for (var i = 0; i < 1000; i++) { + writer.writeString(''); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readString(0); + } + reader.reset(); + } +} + +/// Benchmark for realistic message protocol with strings +/// +/// Simulates reading a typical JSON-like message structure with +/// multiple string fields of varying types and lengths. +class RealisticMessageReadBenchmark extends BenchmarkBase { + RealisticMessageReadBenchmark() : super('String read: realistic message'); + + late BinaryReader reader; + late Uint8List buffer; + late List fieldLengths; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 32768); + + // Typical message fields + const fields = [ + 'user', // Field name (ASCII) + 'John Doe', // Value (ASCII) + 'email', // Field name (ASCII) + 'john.doe@example.com', // Value (ASCII) + 'message', // Field name (ASCII) + 'Hello 世界! 🌍', // Value (mixed Unicode) + 'timestamp', // Field name (ASCII) + '2024-12-30T12:00:00Z', // Value (ASCII) + 'locale', // Field name (ASCII) + 'ru-RU', // Value (ASCII) + ]; + + fieldLengths = fields.map(getUtf8Length).toList(); + + // Write 100 messages + for (var i = 0; i < 100; i++) { + fields.forEach(writer.writeString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + fieldLengths.forEach(reader.readString); + } + reader.reset(); + } +} + +/// Benchmark for alternating short and long strings +class AlternatingStringReadBenchmark extends BenchmarkBase { + AlternatingStringReadBenchmark() : super('String read: alternating lengths'); + + late BinaryReader reader; + late Uint8List buffer; + late int shortLength; + late int longLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 32768); + const shortString = 'Hi'; + const longString = + 'This is a much longer string with more content to read and process'; + + shortLength = shortString.length; + longLength = longString.length; + + // Alternate between short and long strings + for (var i = 0; i < 500; i++) { + writer + ..writeString(shortString) + ..writeString(longString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + reader + ..readString(shortLength) + ..readString(longLength); + } + reader.reset(); + } +} + +/// Benchmark for reading very long strings (> 1KB) +class VeryLongStringReadBenchmark extends BenchmarkBase { + VeryLongStringReadBenchmark() : super('String read: very long (>1KB)'); + + late BinaryReader reader; + late Uint8List buffer; + late int stringLength; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 65536); + // Create a ~2KB string + final longString = 'Lorem ipsum dolor sit amet. ' * 80; + stringLength = longString.length; + + // Write 50 very long strings + for (var i = 0; i < 50; i++) { + writer.writeString(longString); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 50; i++) { + reader.readString(stringLength); + } + reader.reset(); + } +} + +void main() { + test('ASCII string benchmarks:', () { + AsciiStringReadBenchmark().report(); + ShortAsciiStringReadBenchmark().report(); + LongAsciiStringReadBenchmark().report(); + EmptyStringReadBenchmark().report(); + }, tags: ['benchmark']); + + test('UTF-8 multi-byte benchmarks:', () { + CyrillicStringReadBenchmark().report(); + CjkStringReadBenchmark().report(); + EmojiStringReadBenchmark().report(); + MixedUnicodeStringReadBenchmark().report(); + }, tags: ['benchmark']); + + test('VarString benchmarks:', () { + VarStringAsciiReadBenchmark().report(); + VarStringMixedReadBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic string scenarios:', () { + RealisticMessageReadBenchmark().report(); + AlternatingStringReadBenchmark().report(); + VeryLongStringReadBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/micro/reader/varint_read_bench.dart b/test/performance/micro/reader/varint_read_bench.dart new file mode 100644 index 0000000..138c42c --- /dev/null +++ b/test/performance/micro/reader/varint_read_bench.dart @@ -0,0 +1,318 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for reading VarUint in fast path (single byte: 0-127) +/// +/// This is the most common case in real-world protocols where small numbers +/// (lengths, counts, small IDs) dominate. The fast path should be highly +/// optimized as it's hit most frequently. +class VarUintFastPathBenchmark extends BenchmarkBase { + VarUintFastPathBenchmark() : super('VarUint read: 0-127 (fast path)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 single-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(i % 128); // Values 0-127 + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 2-byte VarUint (128-16383) +/// +/// Second most common case - covers most typical array lengths, +/// message sizes, and medium-range IDs. +class VarUint2ByteBenchmark extends BenchmarkBase { + VarUint2ByteBenchmark() : super('VarUint read: 128-16383 (2 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 two-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(128 + (i % 100)); // Values 128-227 + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 3-byte VarUint (16384-2097151) +class VarUint3ByteBenchmark extends BenchmarkBase { + VarUint3ByteBenchmark() : super('VarUint read: 16384-2097151 (3 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 three-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(16384 + (i % 1000) * 100); + } + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 4-byte VarUint (2097152-268435455) +class VarUint4ByteBenchmark extends BenchmarkBase { + VarUint4ByteBenchmark() : super('VarUint read: 2097152-268435455 (4 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 four-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(2097152 + (i % 1000) * 10000); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading 5-byte VarUint (268435456+) +/// +/// Less common in practice but important for large file sizes, +/// timestamps, or 64-bit IDs. +class VarUint5ByteBenchmark extends BenchmarkBase { + VarUint5ByteBenchmark() : super('VarUint read: 268435456+ (5 bytes)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 five-byte VarUints + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(268435456 + i * 1000000); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarUint(); + } + + reader.reset(); + } +} + +/// Benchmark for reading VarInt with ZigZag encoding (small positive values) +/// +/// ZigZag encoding: 0=>0, 1=>2, 2=>4, etc. +/// Tests decoding performance for positive signed integers. +class VarIntPositiveBenchmark extends BenchmarkBase { + VarIntPositiveBenchmark() : super('VarInt read: positive (ZigZag)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 positive VarInts + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(i % 1000); // Values 0-999 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarInt(); + } + reader.reset(); + } +} + +/// Benchmark for reading VarInt with ZigZag encoding (small negative values) +/// +/// ZigZag encoding: -1=>1, -2=>3, -3=>5, etc. +/// Tests decoding performance for negative signed integers. +class VarIntNegativeBenchmark extends BenchmarkBase { + VarIntNegativeBenchmark() : super('VarInt read: negative (ZigZag)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 negative VarInts + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(-(i % 1000 + 1)); // Values -1 to -1000 + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final _ = reader.readVarInt(); + } + + reader.reset(); + } +} + +/// Benchmark for reading mixed VarInt values (positive and negative) +/// +/// Realistic scenario where data contains both positive and negative values. +class VarIntMixedBenchmark extends BenchmarkBase { + VarIntMixedBenchmark() : super('VarInt read: mixed positive/negative'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(initialBufferSize: 8192); + // Write 1000 mixed VarInts + for (var i = 0; i < 1000; i++) { + final value = i.isEven ? (i ~/ 2) % 100 : -((i ~/ 2) % 100 + 1); + writer.writeVarInt(value); + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarInt(); + } + + reader.reset(); + } +} + +/// Benchmark for reading mixed sizes VarUint (realistic distribution) +/// +/// Simulates real-world usage where most values are small (1-2 bytes) +/// but occasionally large values appear. +/// Distribution: 70% single-byte, 20% two-byte, 8% three-byte, 2% four-byte+ +class VarUintMixedSizesBenchmark extends BenchmarkBase { + VarUintMixedSizesBenchmark() : super('VarUint read: mixed sizes (realistic)'); + + late BinaryReader reader; + late Uint8List buffer; + + @override + void setup() { + final writer = BinaryWriter(); + // Write 1000 VarUints with realistic distribution + for (var i = 0; i < 1000; i++) { + final mod = i % 100; + if (mod < 70) { + // 70% single byte + writer.writeVarUint(i % 128); + } else if (mod < 90) { + // 20% two bytes + writer.writeVarUint(128 + (i % 1000)); + } else if (mod < 98) { + // 8% three bytes + writer.writeVarUint(16384 + (i % 10000)); + } else { + // 2% four+ bytes + writer.writeVarUint(2097152 + i * 1000); + } + } + + buffer = writer.takeBytes(); + reader = BinaryReader(buffer); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + reader.readVarUint(); + } + + reader.reset(); + } +} + +void main() { + test('VarUint size benchmarks:', () { + VarUintFastPathBenchmark().report(); + VarUint2ByteBenchmark().report(); + VarUint3ByteBenchmark().report(); + VarUint4ByteBenchmark().report(); + VarUint5ByteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarInt (ZigZag) benchmarks:', () { + VarIntPositiveBenchmark().report(); + VarIntNegativeBenchmark().report(); + VarIntMixedBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios:', () { + VarUintMixedSizesBenchmark().report(); + }, tags: ['benchmark']); +} From 90260740d5ffdd69853d7fdb919a9500cc6d7b15 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 2 Jan 2026 11:12:19 +0200 Subject: [PATCH 20/29] Add performance benchmarks for binary writing operations - Implement benchmarks for writing fixed-size integers (Uint8, Int8, Uint16, Int16, Uint32, Int32, Uint64, Int64) with both big-endian and little-endian formats. - Create benchmarks for writing floating-point numbers (Float32, Float64) including various scenarios such as small values, large values, and special values. - Introduce benchmarks for string writing, covering ASCII, Cyrillic, CJK, emoji, and mixed Unicode strings, as well as VarString scenarios. - Add benchmarks for VarInt and VarUint writing, including fast paths and mixed sizes. - Implement a pool of BinaryWriter benchmarks to evaluate performance in acquiring, releasing, and reusing writers. --- dart_test.yaml | 5 + .../binary_reader_performance_test.dart | 119 ------ .../binary_writer_performance_test.dart | 159 -------- .../{micro => }/reader/binary_read_bench.dart | 0 .../reader/fixed_int_read_bench.dart | 0 .../{micro => }/reader/float_read_bench.dart | 0 .../{micro => }/reader/navigation_bench.dart | 0 .../{micro => }/reader/string_read_bench.dart | 0 .../{micro => }/reader/varint_read_bench.dart | 0 .../writer/binary_write_bench.dart | 332 +++++++++++++++++ .../writer/buffer_growth_bench.dart | 320 ++++++++++++++++ .../writer/fixed_int_write_bench.dart | 345 ++++++++++++++++++ .../performance/writer/float_write_bench.dart | 299 +++++++++++++++ test/performance/writer/pool_bench.dart | 302 +++++++++++++++ .../writer/string_write_bench.dart | 321 ++++++++++++++++ .../writer/varint_write_bench.dart | 215 +++++++++++ 16 files changed, 2139 insertions(+), 278 deletions(-) delete mode 100644 test/performance/binary_reader_performance_test.dart delete mode 100644 test/performance/binary_writer_performance_test.dart rename test/performance/{micro => }/reader/binary_read_bench.dart (100%) rename test/performance/{micro => }/reader/fixed_int_read_bench.dart (100%) rename test/performance/{micro => }/reader/float_read_bench.dart (100%) rename test/performance/{micro => }/reader/navigation_bench.dart (100%) rename test/performance/{micro => }/reader/string_read_bench.dart (100%) rename test/performance/{micro => }/reader/varint_read_bench.dart (100%) create mode 100644 test/performance/writer/binary_write_bench.dart create mode 100644 test/performance/writer/buffer_growth_bench.dart create mode 100644 test/performance/writer/fixed_int_write_bench.dart create mode 100644 test/performance/writer/float_write_bench.dart create mode 100644 test/performance/writer/pool_bench.dart create mode 100644 test/performance/writer/string_write_bench.dart create mode 100644 test/performance/writer/varint_write_bench.dart diff --git a/dart_test.yaml b/dart_test.yaml index 6c78dc3..b0d1859 100644 --- a/dart_test.yaml +++ b/dart_test.yaml @@ -5,3 +5,8 @@ tags: benchmark: description: Performance/benchmark tests (excluded from CI by default). + +# Include benchmark files in test discovery +# Note: By default, only *_test.dart files are discovered. +# This configuration allows *_bench.dart files to be found too. +filename: "*_{test,bench}.dart" diff --git a/test/performance/binary_reader_performance_test.dart b/test/performance/binary_reader_performance_test.dart deleted file mode 100644 index 30479b7..0000000 --- a/test/performance/binary_reader_performance_test.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:benchmark_harness/benchmark_harness.dart'; -import 'package:pro_binary/pro_binary.dart'; -import 'package:test/test.dart'; - -const string = 'Hello, World!'; -const longString = - 'The quick brown fox 🦊 jumps over the lazy dog 🐕. ' - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit 🔬. ' - 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua 🏋️. ' - 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' - 'ut aliquip ex ea commodo consequat ☕. ' - 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' - 'dolore eu fugiat nulla pariatur 🌈. ' - 'Excepteur sint occaecat cupidatat non proident, ' - 'sunt in culpa qui officia deserunt mollit anim id est laborum. 🎯 ' - '🚀 TEST EXTENSION: Adding a second long paragraph to truly stress the ' - 'UTF-8 encoding logic. This includes more complex characters like the ' - 'Zodiac signs ♒️ ♓️ ♈️ ♉️ and some CJK characters like 日本語. ' - 'We also add a few more standard 4-byte emoji like a stack of money 💰, ' - 'a ghost 👻, and a classic thumbs up 👍 to ensure maximum complexity ' - 'in the string encoding process. The purpose of this extra length is to ' - 'force the `_ensureSize` method to be called multiple times and ensure ' - 'that the buffer resizing and copying overhead is measured correctly. ' - 'This paragraph is deliberately longer to ensure that the total byte ' - 'count for UTF-8 is significantly larger than the initial string length. ' - '🏁'; - -class BinaryReaderBenchmark extends BenchmarkBase { - BinaryReaderBenchmark() : super('BinaryReader performance test'); - - late final BinaryReader reader; - - @override - void setup() { - final writer = BinaryWriter() - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535, .little) - ..writeInt16(-32768, .little) - ..writeUint32(4294967295, .little) - ..writeInt32(-2147483648, .little) - ..writeUint64(9223372036854775807, .little) - ..writeInt64(-9223372036854775808, .little) - ..writeFloat32(3.14, .little) - ..writeFloat64(3.141592653589793, .little) - ..writeFloat64(2.718281828459045) - ..writeVarString(string) - ..writeVarString(longString) - ..writeBytes([]) - ..writeBytes(List.filled(120, 100)); - - final buffer = writer.takeBytes(); - reader = BinaryReader(buffer); - } - - @override - void exercise() => run(); - - @override - void run() { - for (var i = 0; i < 1000; i++) { - final _ = reader.readUint8(); - final _ = reader.readInt8(); - final _ = reader.readUint16(.little); - final _ = reader.readInt16(.little); - final _ = reader.readUint32(.little); - final _ = reader.readInt32(.little); - final _ = reader.readUint64(.little); - final _ = reader.readInt64(.little); - final _ = reader.readFloat32(.little); - final _ = reader.readFloat64(.little); - final _ = reader.readFloat64(.little); - final _ = reader.readVarString(); - final _ = reader.readVarString(); - final _ = reader.readBytes(0); - final _ = reader.readBytes(120); - - assert(reader.availableBytes == 0, 'Not all bytes were read'); - reader.reset(); - } - } -} - -class GetStringLengthBenchmark extends BenchmarkBase { - GetStringLengthBenchmark() : super('GetStringLength performance test'); - - @override - void exercise() => run(); - - @override - void run() { - for (var i = 0; i < 1000; i++) { - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - final _ = getUtf8Length(string); - final _ = getUtf8Length(longString); - } - } -} - -void main() { - test('BinaryReaderBenchmark', () { - BinaryReaderBenchmark().report(); - }, tags: ['benchmark']); - - test('GetStringLengthBenchmark', () { - GetStringLengthBenchmark().report(); - }, tags: ['benchmark']); -} diff --git a/test/performance/binary_writer_performance_test.dart b/test/performance/binary_writer_performance_test.dart deleted file mode 100644 index 9c268f0..0000000 --- a/test/performance/binary_writer_performance_test.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'dart:typed_data'; - -import 'package:benchmark_harness/benchmark_harness.dart'; -import 'package:pro_binary/pro_binary.dart'; -import 'package:test/test.dart'; - -const longStringWithEmoji = - 'The quick brown fox 🦊 jumps over the lazy dog 🐕. ' - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit 🔬. ' - 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua 🏋️. ' - 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' - 'ut aliquip ex ea commodo consequat ☕. ' - 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum ' - 'dolore eu fugiat nulla pariatur 🌈. ' - 'Excepteur sint occaecat cupidatat non proident, ' - 'sunt in culpa qui officia deserunt mollit anim id est laborum. 🎯 ' - '🚀 TEST EXTENSION: Adding a second long paragraph to truly stress the ' - 'UTF-8 encoding logic. This includes more complex characters like the ' - 'Zodiac signs ♒️ ♓️ ♈️ ♉️ and some CJK characters like 日本語. ' - 'We also add a few more standard 4-byte emoji like a stack of money 💰, ' - 'a ghost 👻, and a classic thumbs up 👍 to ensure maximum complexity ' - 'in the string encoding process. The purpose of this extra length is to ' - 'force the `_ensureSize` method to be called multiple times and ensure ' - 'that the buffer resizing and copying overhead is measured correctly. ' - 'This paragraph is deliberately longer to ensure that the total byte ' - 'count for UTF-8 is significantly larger than the initial string length. ' - '🏁'; - -const shortString = 'Hello, World!'; - -final listUint8 = Uint8List.fromList([ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 200, 255, 0, 128, 64, // -]); - -final listUint16 = Uint16List.fromList([ - 1, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65535, // -]); - -final listUint32 = Uint32List.fromList([ - 1, 65536, 131072, 262144, 524288, 1048576, 2097152, 4194304, 8388608, - 16777216, 33554432, 67108864, 134217728, 268435456, 536870912, 1073741824, - 2147483648, 4294967295, // -]); - -final listFloat32 = Float32List.fromList([ - 3.14, 2.71, 1.618, 0.5772, 1.4142, 0.6931, 2.3025, 1.732, 0.0, -1.0, -3.14, // -]).buffer.asUint8List(); - -class BinaryWriterBenchmark extends BenchmarkBase { - BinaryWriterBenchmark() : super('BinaryWriter performance test'); - - late final BinaryWriter writer; - - @override - void setup() { - writer = BinaryWriter(); - } - - @override - void run() { - for (var i = 0; i < 1000; i++) { - writer - ..writeUint8(42) - ..writeInt8(-42) - ..writeUint16(65535, .little) - ..writeUint16(10) - ..writeInt16(-32768, .little) - ..writeInt16(-10) - ..writeUint32(4294967295, .little) - ..writeUint32(100) - ..writeInt32(-2147483648, .little) - ..writeInt32(-100) - ..writeUint64(9223372036854775807, .little) - ..writeUint64(1000) - ..writeInt64(-9223372036854775808, .little) - ..writeInt64(-1000) - ..writeFloat32(3.14, .little) - ..writeFloat32(2.71) - ..writeFloat64(3.141592653589793, .little) - ..writeFloat64(2.718281828459045) - ..writeBytes(listUint8) - ..writeBytes(listUint16) - ..writeBytes(listUint32) - ..writeBytes(listFloat32) - ..writeString(shortString) - ..writeString(longStringWithEmoji); - - final bytes = writer.takeBytes(); - - if (writer.bytesWritten != 0) { - throw StateError('bytesWritten should be reset to 0 after takeBytes()'); - } - - if (bytes.length != 1432) { - throw StateError('Unexpected byte length: ${bytes.length}'); - } - } - } - - @override - void exercise() => run(); -} - -class BinaryWriterVarIntBenchmark extends BenchmarkBase { - BinaryWriterVarIntBenchmark() : super('BinaryWriter performance test'); - - late final BinaryWriter writer; - - @override - void setup() { - writer = BinaryWriter(); - } - - @override - void run() { - for (var i = 0; i < 1000; i++) { - writer - ..writeVarInt(42) - ..writeVarInt(-42) - ..writeVarInt(512) - ..writeVarInt(-512) - ..writeVarUint(65535) - ..writeVarUint(100) - ..writeVarInt(-32768) - ..writeVarInt(32768) - ..writeVarUint(4294967295) - ..writeVarUint(100) - ..writeVarInt(-2147483648) - ..writeVarInt(2147483647) - ..writeVarUint(9223372036854775807) - ..writeVarUint(1000) - ..writeVarInt(-9223372036854775808) - ..writeVarInt(9223372036854775807); - - final bytes = writer.takeBytes(); - - if (writer.bytesWritten != 0) { - throw StateError('bytesWritten should be reset to 0 after takeBytes()'); - } - - if (bytes.length != 63) { - throw StateError('Unexpected byte length: ${bytes.length}'); - } - } - } - - @override - void exercise() => run(); -} - -void main() { - test('BinaryWriterBenchmark', () { - BinaryWriterBenchmark().report(); - }, tags: ['benchmark']); - - test('BinaryWriterVarIntBenchmark', () { - BinaryWriterVarIntBenchmark().report(); - }, tags: ['benchmark']); -} diff --git a/test/performance/micro/reader/binary_read_bench.dart b/test/performance/reader/binary_read_bench.dart similarity index 100% rename from test/performance/micro/reader/binary_read_bench.dart rename to test/performance/reader/binary_read_bench.dart diff --git a/test/performance/micro/reader/fixed_int_read_bench.dart b/test/performance/reader/fixed_int_read_bench.dart similarity index 100% rename from test/performance/micro/reader/fixed_int_read_bench.dart rename to test/performance/reader/fixed_int_read_bench.dart diff --git a/test/performance/micro/reader/float_read_bench.dart b/test/performance/reader/float_read_bench.dart similarity index 100% rename from test/performance/micro/reader/float_read_bench.dart rename to test/performance/reader/float_read_bench.dart diff --git a/test/performance/micro/reader/navigation_bench.dart b/test/performance/reader/navigation_bench.dart similarity index 100% rename from test/performance/micro/reader/navigation_bench.dart rename to test/performance/reader/navigation_bench.dart diff --git a/test/performance/micro/reader/string_read_bench.dart b/test/performance/reader/string_read_bench.dart similarity index 100% rename from test/performance/micro/reader/string_read_bench.dart rename to test/performance/reader/string_read_bench.dart diff --git a/test/performance/micro/reader/varint_read_bench.dart b/test/performance/reader/varint_read_bench.dart similarity index 100% rename from test/performance/micro/reader/varint_read_bench.dart rename to test/performance/reader/varint_read_bench.dart diff --git a/test/performance/writer/binary_write_bench.dart b/test/performance/writer/binary_write_bench.dart new file mode 100644 index 0000000..a9bb0a8 --- /dev/null +++ b/test/performance/writer/binary_write_bench.dart @@ -0,0 +1,332 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing small byte arrays (< 16 bytes) +class SmallBytesWriteBenchmark extends BenchmarkBase { + SmallBytesWriteBenchmark() : super('Bytes write: small (8 bytes)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing medium byte arrays (64 bytes) +class MediumBytesWriteBenchmark extends BenchmarkBase { + MediumBytesWriteBenchmark() : super('Bytes write: medium (64 bytes)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + data = Uint8List.fromList(List.generate(64, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing large byte arrays (1 KB) +class LargeBytesWriteBenchmark extends BenchmarkBase { + LargeBytesWriteBenchmark() : super('Bytes write: large (1 KB)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 1024 * 1024); + data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing very large byte arrays (64 KB) +class VeryLargeBytesWriteBenchmark extends BenchmarkBase { + VeryLargeBytesWriteBenchmark() : super('Bytes write: very large (64 KB)'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 64 * 1024 * 10); + data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 10; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing VarBytes (length-prefixed byte arrays) +class VarBytesSmallWriteBenchmark extends BenchmarkBase { + VarBytesSmallWriteBenchmark() : super('VarBytes write: small'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing VarBytes with medium-sized data +class VarBytesMediumWriteBenchmark extends BenchmarkBase { + VarBytesMediumWriteBenchmark() : super('VarBytes write: medium'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 256 * 1024); + data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing VarBytes with large data +class VarBytesLargeWriteBenchmark extends BenchmarkBase { + VarBytesLargeWriteBenchmark() : super('VarBytes write: large'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 512 * 1024); + data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing empty byte arrays +class EmptyBytesWriteBenchmark extends BenchmarkBase { + EmptyBytesWriteBenchmark() : super('Bytes write: empty'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes([]); + } + writer.reset(); + } +} + +/// Benchmark for mixed-size byte writes (realistic scenario) +class MixedBytesWriteBenchmark extends BenchmarkBase { + MixedBytesWriteBenchmark() : super('Bytes write: mixed sizes (realistic)'); + + late BinaryWriter writer; + late Uint8List header; + late List payloads; + late Uint8List checksum; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + header = Uint8List.fromList(List.generate(16, (j) => j)); + payloads = [ + Uint8List.fromList(List.generate(64, (j) => j % 256)), + Uint8List.fromList(List.generate(128, (j) => j % 256)), + Uint8List.fromList(List.generate(256, (j) => j % 256)), + ]; + checksum = Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF]); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + ..writeBytes(header) + ..writeBytes(payloads[i % 3]) + ..writeBytes(checksum); + } + writer.reset(); + } +} + +/// Benchmark for alternating small and large writes +class AlternatingBytesWriteBenchmark extends BenchmarkBase { + AlternatingBytesWriteBenchmark() : super('Bytes write: alternating sizes'); + + late BinaryWriter writer; + late Uint8List small; + late Uint8List large; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + small = Uint8List.fromList([1, 2, 3, 4]); + large = Uint8List.fromList(List.generate(512, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + ..writeBytes(small) + ..writeBytes(large); + } + writer.reset(); + } +} + +/// Benchmark for sequential small writes +class SequentialSmallWritesBenchmark extends BenchmarkBase { + SequentialSmallWritesBenchmark() + : super('Bytes write: sequential small writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 4000; i++) { + writer.writeUint8(i % 256); + } + writer.reset(); + } +} + +/// Benchmark for writing bytes from List of int +class ListIntWriteBenchmark extends BenchmarkBase { + ListIntWriteBenchmark() : super('Bytes write: from List'); + + late BinaryWriter writer; + late List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + data = List.generate(64, (i) => i % 256); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for writing bytes from Uint8List view +class Uint8ListViewWriteBenchmark extends BenchmarkBase { + Uint8ListViewWriteBenchmark() : super('Bytes write: Uint8List view'); + + late BinaryWriter writer; + late Uint8List data; + late Uint8List view; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + data = Uint8List.fromList(List.generate(128, (i) => i % 256)); + view = Uint8List.view(data.buffer, 32, 64); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeBytes(view); + } + writer.reset(); + } +} + +void main() { + test('Fixed-size writes benchmarks:', () { + EmptyBytesWriteBenchmark().report(); + SmallBytesWriteBenchmark().report(); + MediumBytesWriteBenchmark().report(); + LargeBytesWriteBenchmark().report(); + VeryLargeBytesWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarBytes (length-prefixed) benchmarks:', () { + VarBytesSmallWriteBenchmark().report(); + VarBytesMediumWriteBenchmark().report(); + VarBytesLargeWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios benchmarks:', () { + MixedBytesWriteBenchmark().report(); + AlternatingBytesWriteBenchmark().report(); + SequentialSmallWritesBenchmark().report(); + }, tags: ['benchmark']); + + test('Special input types benchmarks:', () { + ListIntWriteBenchmark().report(); + Uint8ListViewWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/buffer_growth_bench.dart b/test/performance/writer/buffer_growth_bench.dart new file mode 100644 index 0000000..f71b7e2 --- /dev/null +++ b/test/performance/writer/buffer_growth_bench.dart @@ -0,0 +1,320 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for buffer growth from small initial size +class BufferGrowthSmallInitialBenchmark extends BenchmarkBase { + BufferGrowthSmallInitialBenchmark() + : super('Buffer growth: small initial (16 bytes -> 1KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16); + } + + @override + void run() { + // Write 1KB of data, forcing multiple expansions + for (var i = 0; i < 256; i++) { + writer.writeUint32(i); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth from medium initial size +class BufferGrowthMediumInitialBenchmark extends BenchmarkBase { + BufferGrowthMediumInitialBenchmark() + : super('Buffer growth: medium initial (256 bytes -> 64KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 256); + } + + @override + void run() { + // Write 64KB of data + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + for (var i = 0; i < 256; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with incremental writes +class BufferGrowthIncrementalBenchmark extends BenchmarkBase { + BufferGrowthIncrementalBenchmark() + : super('Buffer growth: incremental writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 64); + } + + @override + void run() { + // Write progressively larger chunks + for (var size = 1; size <= 256; size *= 2) { + final data = Uint8List.fromList(List.generate(size, (i) => i % 256)); + for (var i = 0; i < 10; i++) { + writer.writeBytes(data); + } + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with large single write +class BufferGrowthLargeSingleWriteBenchmark extends BenchmarkBase { + BufferGrowthLargeSingleWriteBenchmark() + : super('Buffer growth: large single write'); + + late BinaryWriter writer; + late Uint8List largeData; + + @override + void setup() { + writer = BinaryWriter(); + largeData = Uint8List.fromList(List.generate(32768, (i) => i % 256)); + } + + @override + void run() { + // Single large write that forces expansion + writer + ..writeBytes(largeData) + ..reset(); + } +} + +/// Benchmark for buffer growth with string writes +class BufferGrowthStringWritesBenchmark extends BenchmarkBase { + BufferGrowthStringWritesBenchmark() : super('Buffer growth: string writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32); + } + + @override + void run() { + const testString = 'Hello World! This is a test string.'; + for (var i = 0; i < 500; i++) { + writer.writeString(testString); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with VarInt writes +class BufferGrowthVarIntWritesBenchmark extends BenchmarkBase { + BufferGrowthVarIntWritesBenchmark() : super('Buffer growth: VarInt writes'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 1024); + } + + @override + void run() { + for (var i = 0; i < 250; i++) { + writer.writeVarUint(i & 0x7F); // Keep values to 0-127 (single byte) + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with mixed writes +class BufferGrowthMixedWritesBenchmark extends BenchmarkBase { + BufferGrowthMixedWritesBenchmark() : super('Buffer growth: mixed data types'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 64); + } + + @override + void run() { + for (var i = 0; i < 200; i++) { + writer + ..writeUint8(i % 256) + ..writeUint32(i * 1000, Endian.little) + ..writeFloat64(i * 3.14, Endian.little) + ..writeString('Message $i') + ..writeVarUint(i); + } + writer.reset(); + } +} + +/// Benchmark for no buffer growth (sufficient initial size) +class NoBufferGrowthBenchmark extends BenchmarkBase { + NoBufferGrowthBenchmark() + : super('No buffer growth: sufficient initial size'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + // Write 32KB without triggering growth + final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); + for (var i = 0; i < 128; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with VarBytes +class BufferGrowthVarBytesBenchmark extends BenchmarkBase { + BufferGrowthVarBytesBenchmark() : super('Buffer growth: VarBytes writes'); + + late BinaryWriter writer; + late Uint8List data; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16 * 1024); + data = Uint8List.fromList(List.generate(32, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarBytes(data); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth pattern: write, reset, write larger +class BufferGrowthResetPatternBenchmark extends BenchmarkBase { + BufferGrowthResetPatternBenchmark() + : super('Buffer growth: write-reset-write pattern'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(); + } + + @override + void run() { + // First write: small + for (var i = 0; i < 16; i++) { + writer.writeUint32(i); + } + writer.reset(); + + // Second write: medium (may reuse buffer) + for (var i = 0; i < 64; i++) { + writer.writeUint32(i); + } + writer.reset(); + + // Third write: large (may grow buffer) + for (var i = 0; i < 256; i++) { + writer.writeUint32(i); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth with alternating sizes +class BufferGrowthAlternatingSizesBenchmark extends BenchmarkBase { + BufferGrowthAlternatingSizesBenchmark() + : super('Buffer growth: alternating write sizes'); + + late BinaryWriter writer; + late Uint8List smallData; + late Uint8List largeData; + + @override + void setup() { + writer = BinaryWriter(); + smallData = Uint8List.fromList(List.generate(8, (i) => i)); + largeData = Uint8List.fromList(List.generate(512, (i) => i % 256)); + } + + @override + void run() { + for (var i = 0; i < 50; i++) { + writer + ..writeBytes(smallData) + ..writeBytes(largeData) + ..writeBytes(smallData); + } + writer.reset(); + } +} + +/// Benchmark for buffer growth reaching max reusable capacity +class BufferGrowthMaxCapacityBenchmark extends BenchmarkBase { + BufferGrowthMaxCapacityBenchmark() + : super('Buffer growth: reaching max capacity (64KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 1024); + } + + @override + void run() { + // Write exactly 64KB to test max reusable capacity + final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); + for (var i = 0; i < 64; i++) { + writer.writeBytes(data); + } + writer.reset(); + } +} + +void main() { + test('Initial size variations:', () { + BufferGrowthSmallInitialBenchmark().report(); + BufferGrowthMediumInitialBenchmark().report(); + NoBufferGrowthBenchmark().report(); + }, tags: ['benchmark']); + + test('Growth patterns:', () { + BufferGrowthIncrementalBenchmark().report(); + BufferGrowthLargeSingleWriteBenchmark().report(); + BufferGrowthAlternatingSizesBenchmark().report(); + }, tags: ['benchmark']); + + test('Data type specific growth:', () { + BufferGrowthStringWritesBenchmark().report(); + BufferGrowthVarIntWritesBenchmark().report(); + BufferGrowthVarBytesBenchmark().report(); + BufferGrowthMixedWritesBenchmark().report(); + }, tags: ['benchmark']); + + test('Reset and capacity patterns:', () { + BufferGrowthResetPatternBenchmark().report(); + BufferGrowthMaxCapacityBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/fixed_int_write_bench.dart b/test/performance/writer/fixed_int_write_bench.dart new file mode 100644 index 0000000..e23f578 --- /dev/null +++ b/test/performance/writer/fixed_int_write_bench.dart @@ -0,0 +1,345 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing Uint8 +class Uint8WriteBenchmark extends BenchmarkBase { + Uint8WriteBenchmark() : super('Uint8 write'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint8(i % 256); + } + writer.reset(); + } +} + +/// Benchmark for writing Int8 +class Int8WriteBenchmark extends BenchmarkBase { + Int8WriteBenchmark() : super('Int8 write'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt8((i % 256) - 128); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint16 big-endian +class Uint16BigEndianWriteBenchmark extends BenchmarkBase { + Uint16BigEndianWriteBenchmark() : super('Uint16 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint16(i % 65536); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint16 little-endian +class Uint16LittleEndianWriteBenchmark extends BenchmarkBase { + Uint16LittleEndianWriteBenchmark() : super('Uint16 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint16(i % 65536, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Int16 big-endian +class Int16BigEndianWriteBenchmark extends BenchmarkBase { + Int16BigEndianWriteBenchmark() : super('Int16 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i % 65536) - 32768); + } + writer.reset(); + } +} + +/// Benchmark for writing Int16 little-endian +class Int16LittleEndianWriteBenchmark extends BenchmarkBase { + Int16LittleEndianWriteBenchmark() : super('Int16 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt16((i % 65536) - 32768, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint32 big-endian +class Uint32BigEndianWriteBenchmark extends BenchmarkBase { + Uint32BigEndianWriteBenchmark() : super('Uint32 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint32(i * 1000); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint32 little-endian +class Uint32LittleEndianWriteBenchmark extends BenchmarkBase { + Uint32LittleEndianWriteBenchmark() : super('Uint32 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint32(i * 1000, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Int32 big-endian +class Int32BigEndianWriteBenchmark extends BenchmarkBase { + Int32BigEndianWriteBenchmark() : super('Int32 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt32(i * 1000 - 500000); + } + writer.reset(); + } +} + +/// Benchmark for writing Int32 little-endian +class Int32LittleEndianWriteBenchmark extends BenchmarkBase { + Int32LittleEndianWriteBenchmark() : super('Int32 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt32(i * 1000 - 500000, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint64 big-endian +class Uint64BigEndianWriteBenchmark extends BenchmarkBase { + Uint64BigEndianWriteBenchmark() : super('Uint64 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000); + } + writer.reset(); + } +} + +/// Benchmark for writing Uint64 little-endian +class Uint64LittleEndianWriteBenchmark extends BenchmarkBase { + Uint64LittleEndianWriteBenchmark() : super('Uint64 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeUint64(i * 1000000, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Int64 big-endian +class Int64BigEndianWriteBenchmark extends BenchmarkBase { + Int64BigEndianWriteBenchmark() : super('Int64 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt64(i * 1000000 - 500000000); + } + writer.reset(); + } +} + +/// Benchmark for writing Int64 little-endian +class Int64LittleEndianWriteBenchmark extends BenchmarkBase { + Int64LittleEndianWriteBenchmark() : super('Int64 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeInt64(i * 1000000 - 500000000, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for mixed fixed-int writes (realistic scenario) +class MixedFixedIntWriteBenchmark extends BenchmarkBase { + MixedFixedIntWriteBenchmark() : super('Mixed fixed-int write (realistic)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + ..writeUint8(i % 256) + ..writeUint16(i % 65536, Endian.little) + ..writeUint32(i * 1000, Endian.little) + ..writeInt32(i * 100 - 5000, Endian.little) + ..writeUint64(i * 1000000, Endian.little) + ..writeInt8((i % 256) - 128) + ..writeInt16((i % 32768) - 16384, Endian.little) + ..writeInt64(i * 1000000, Endian.little); + } + writer.reset(); + } +} + +void main() { + test('8-bit integer benchmarks:', () { + Uint8WriteBenchmark().report(); + Int8WriteBenchmark().report(); + }, tags: ['benchmark']); + + test('16-bit integer benchmarks:', () { + Uint16BigEndianWriteBenchmark().report(); + Uint16LittleEndianWriteBenchmark().report(); + Int16BigEndianWriteBenchmark().report(); + Int16LittleEndianWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('32-bit integer benchmarks:', () { + Uint32BigEndianWriteBenchmark().report(); + Uint32LittleEndianWriteBenchmark().report(); + Int32BigEndianWriteBenchmark().report(); + Int32LittleEndianWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('64-bit integer benchmarks:', () { + Uint64BigEndianWriteBenchmark().report(); + Uint64LittleEndianWriteBenchmark().report(); + Int64BigEndianWriteBenchmark().report(); + Int64LittleEndianWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed integer benchmarks:', () { + MixedFixedIntWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/float_write_bench.dart b/test/performance/writer/float_write_bench.dart new file mode 100644 index 0000000..385a82f --- /dev/null +++ b/test/performance/writer/float_write_bench.dart @@ -0,0 +1,299 @@ +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing Float32 big-endian +class Float32BigEndianWriteBenchmark extends BenchmarkBase { + Float32BigEndianWriteBenchmark() : super('Float32 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i * 3.14159) - 500.0); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 little-endian +class Float32LittleEndianWriteBenchmark extends BenchmarkBase { + Float32LittleEndianWriteBenchmark() : super('Float32 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i * 3.14159) - 500.0, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 small values +class Float32SmallValuesWriteBenchmark extends BenchmarkBase { + Float32SmallValuesWriteBenchmark() : super('Float32 write (small values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i % 100) * 0.01, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 large values +class Float32LargeValuesWriteBenchmark extends BenchmarkBase { + Float32LargeValuesWriteBenchmark() : super('Float32 write (large values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat32((i * 1000000.0) - 500000000.0, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float32 special values +class Float32SpecialValuesWriteBenchmark extends BenchmarkBase { + Float32SpecialValuesWriteBenchmark() + : super('Float32 write (special values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 250; i++) { + writer + ..writeFloat32(0, Endian.little) + ..writeFloat32(double.nan, Endian.little) + ..writeFloat32(double.infinity, Endian.little) + ..writeFloat32(double.negativeInfinity, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 big-endian +class Float64BigEndianWriteBenchmark extends BenchmarkBase { + Float64BigEndianWriteBenchmark() : super('Float64 write (big-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64((i * 2.718281828) - 1000.0); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 little-endian +class Float64LittleEndianWriteBenchmark extends BenchmarkBase { + Float64LittleEndianWriteBenchmark() : super('Float64 write (little-endian)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64((i * 2.718281828) - 1000.0, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 small values +class Float64SmallValuesWriteBenchmark extends BenchmarkBase { + Float64SmallValuesWriteBenchmark() : super('Float64 write (small values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64((i % 100) * 0.001, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 large values +class Float64LargeValuesWriteBenchmark extends BenchmarkBase { + Float64LargeValuesWriteBenchmark() : super('Float64 write (large values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeFloat64( + (i * 1000000000.0) - 500000000000.0, + Endian.little, + ); + } + writer.reset(); + } +} + +/// Benchmark for writing Float64 special values +class Float64SpecialValuesWriteBenchmark extends BenchmarkBase { + Float64SpecialValuesWriteBenchmark() + : super('Float64 write (special values)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 250; i++) { + writer + ..writeFloat64(0, Endian.little) + ..writeFloat64(double.nan, Endian.little) + ..writeFloat64(double.infinity, Endian.little) + ..writeFloat64(double.negativeInfinity, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for mixed float writes (realistic scenario) +class MixedFloatWriteBenchmark extends BenchmarkBase { + MixedFloatWriteBenchmark() : super('Mixed float write (realistic)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + // Position (3D coordinates) + ..writeFloat32(i * 10.0, Endian.little) + ..writeFloat32(i * 20.0, Endian.little) + ..writeFloat32(i * 30.0, Endian.little) + // Rotation (quaternion) + ..writeFloat32(0, Endian.little) + ..writeFloat32(0, Endian.little) + ..writeFloat32(0, Endian.little) + ..writeFloat32(1, Endian.little) + // Timestamp + ..writeFloat64(i * 0.016, Endian.little) + // Color (RGBA) + ..writeFloat32(1, Endian.little) + ..writeFloat32(0.5, Endian.little) + ..writeFloat32(0, Endian.little) + ..writeFloat32(1, Endian.little); + } + writer.reset(); + } +} + +/// Benchmark for alternating Float32/Float64 +class AlternatingFloatWriteBenchmark extends BenchmarkBase { + AlternatingFloatWriteBenchmark() : super('Alternating Float32/Float64 write'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 500; i++) { + writer + ..writeFloat32(i * 3.14, Endian.little) + ..writeFloat64(i * 2.718, Endian.little); + } + writer.reset(); + } +} + +void main() { + test('Float32 benchmarks:', () { + Float32BigEndianWriteBenchmark().report(); + Float32LittleEndianWriteBenchmark().report(); + Float32SmallValuesWriteBenchmark().report(); + Float32LargeValuesWriteBenchmark().report(); + Float32SpecialValuesWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Float64 benchmarks:', () { + Float64BigEndianWriteBenchmark().report(); + Float64LittleEndianWriteBenchmark().report(); + Float64SmallValuesWriteBenchmark().report(); + Float64LargeValuesWriteBenchmark().report(); + Float64SpecialValuesWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Mixed float benchmarks:', () { + MixedFloatWriteBenchmark().report(); + AlternatingFloatWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/pool_bench.dart b/test/performance/writer/pool_bench.dart new file mode 100644 index 0000000..f5f92ef --- /dev/null +++ b/test/performance/writer/pool_bench.dart @@ -0,0 +1,302 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for acquiring writers from pool (empty pool) +/// +/// Tests the performance of getting a new writer from the pool. +class PoolAcquireNewBenchmark extends BenchmarkBase { + PoolAcquireNewBenchmark() : super('Pool: acquire new writer'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + final writer = BinaryWriterPool.acquire(); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for acquiring reused writers from pool +/// +/// Tests the performance when writers are reused from the pool. +class PoolAcquireReusedBenchmark extends BenchmarkBase { + PoolAcquireReusedBenchmark() : super('Pool: acquire reused writer'); + + late List writers; + + @override + void setup() { + BinaryWriterPool.clear(); + writers = []; + // Pre-fill pool with released writers + for (var i = 0; i < 10; i++) { + final writer = BinaryWriterPool.acquire() + ..writeBytes(List.generate(100, (j) => j % 256)); + writers.add(writer); + } + writers.forEach(BinaryWriterPool.release); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + final writer = BinaryWriterPool.acquire(); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for releasing writers to pool +/// +/// Tests the performance of returning writers to the pool. +class PoolReleaseBenchmark extends BenchmarkBase { + PoolReleaseBenchmark() : super('Pool: release writer'); + + late List writers; + + @override + void setup() { + BinaryWriterPool.clear(); + writers = []; + for (var i = 0; i < 100; i++) { + writers.add(BinaryWriterPool.acquire()); + } + } + + @override + void run() { + for (final writer in writers) { + writer.writeBytes(List.generate(50, (j) => j % 256)); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for acquire + write + release cycle +/// +/// Full cycle: get writer, use it, return it to pool. +class PoolFullCycleBenchmark extends BenchmarkBase { + PoolFullCycleBenchmark() + : super('Pool: full cycle (acquire + write + release)'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + final writer = BinaryWriterPool.acquire() + ..writeUint32(i) + ..writeString('test message $i') + ..writeBytes(List.generate(32, (j) => (i + j) % 256)); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for heavy writer usage with pool +/// +/// Simulates typical protocol message serialization using pool. +class PoolHeavyUsageBenchmark extends BenchmarkBase { + PoolHeavyUsageBenchmark() : super('Pool: heavy usage (realistic)'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void run() { + for (var i = 0; i < 50; i++) { + final writer = BinaryWriterPool.acquire() + // Simulate message header + ..writeUint32(i) // Message ID + ..writeVarUint(i % 1000) // Message length + // Write payload + ..writeString('Header: $i'); + for (var j = 0; j < 5; j++) { + writer.writeFloat64(i * 3.14 + j); + } + writer.writeBytes(List.generate(256, (k) => (i + k) % 256)); + // Return to pool + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for sequential acquire operations +/// +/// Tests pool performance under sequential load without much release. +class PoolSequentialAcquireBenchmark extends BenchmarkBase { + PoolSequentialAcquireBenchmark() : super('Pool: sequential acquire'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void run() { + final writers = []; + // Acquire up to pool max size + for (var i = 0; i < 32; i++) { + writers.add(BinaryWriterPool.acquire()); + } + // Release all + writers.forEach(BinaryWriterPool.release); + } +} + +/// Benchmark for pool statistics queries +/// +/// Tests the performance of checking pool statistics. +class PoolStatisticsBenchmark extends BenchmarkBase { + PoolStatisticsBenchmark() : super('Pool: query statistics'); + + late List writers; + + @override + void setup() { + BinaryWriterPool.clear(); + writers = []; + for (var i = 0; i < 10; i++) { + final w = BinaryWriterPool.acquire() + ..writeBytes(List.generate(100, (j) => j % 256)); + writers.add(w); + } + } + + @override + void run() { + // Query statistics multiple times + for (var i = 0; i < 1000; i++) { + // This should ideally be cheap - just reading counters + final stat = BinaryWriterPool.stats; + // Use the stat to prevent optimization away + if (stat.pooled > 0) { + // Just to use the value + } + } + } +} + +/// Benchmark for mixed operations on pool +/// +/// Realistic pattern: acquire, use, release in varying patterns. +class PoolMixedOperationsBenchmark extends BenchmarkBase { + PoolMixedOperationsBenchmark() : super('Pool: mixed operations'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void run() { + final batch1 = []; + // Acquire batch + for (var i = 0; i < 10; i++) { + batch1.add(BinaryWriterPool.acquire()); + } + // Use first batch + for (final w in batch1) { + w.writeVarUint(42); + } + // Acquire second batch while first still active + final batch2 = []; + for (var i = 0; i < 10; i++) { + batch2.add(BinaryWriterPool.acquire()); + } + // Release first batch + batch1.forEach(BinaryWriterPool.release); + // Continue using second batch + for (final w in batch2) { + w.writeFloat32(3.14); + } + // Release second batch + batch2.forEach(BinaryWriterPool.release); + } +} + +/// Benchmark for pool with buffer reuse +/// +/// Tests how well buffers are reused when writers are recycled. +class PoolBufferReuseBenchmark extends BenchmarkBase { + PoolBufferReuseBenchmark() : super('Pool: buffer reuse efficiency'); + + @override + void setup() { + BinaryWriterPool.clear(); + } + + @override + void run() { + // Use pool with varying write sizes + for (var cycle = 0; cycle < 20; cycle++) { + final writer = BinaryWriterPool.acquire(); + // Write varying amount of data + final size = 64 * (cycle % 10 + 1); // 64, 128, 192, ..., 640 + writer.writeBytes(List.generate(size, (i) => i % 256)); + BinaryWriterPool.release(writer); + } + } +} + +/// Benchmark for reset statistics +/// +/// Tests the cost of resetting pool statistics. +class PoolResetStatisticsBenchmark extends BenchmarkBase { + PoolResetStatisticsBenchmark() : super('Pool: reset statistics'); + + @override + void setup() { + BinaryWriterPool.clear(); + // Generate some statistics by using pool + for (var i = 0; i < 100; i++) { + final w = BinaryWriterPool.acquire()..writeUint32(i); + BinaryWriterPool.release(w); + } + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + BinaryWriterPool.clear(); + // Do some work + final w = BinaryWriterPool.acquire()..writeUint32(i); + BinaryWriterPool.release(w); + } + } +} + +void main() { + test('Pool acquire operations:', () { + PoolAcquireNewBenchmark().report(); + PoolAcquireReusedBenchmark().report(); + }, tags: ['benchmark']); + + test('Pool release operations:', () { + PoolReleaseBenchmark().report(); + PoolFullCycleBenchmark().report(); + }, tags: ['benchmark']); + + test('Pool usage patterns:', () { + PoolHeavyUsageBenchmark().report(); + PoolSequentialAcquireBenchmark().report(); + PoolMixedOperationsBenchmark().report(); + }, tags: ['benchmark']); + + test('Pool efficiency:', () { + PoolBufferReuseBenchmark().report(); + PoolStatisticsBenchmark().report(); + PoolResetStatisticsBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/string_write_bench.dart b/test/performance/writer/string_write_bench.dart new file mode 100644 index 0000000..fd87118 --- /dev/null +++ b/test/performance/writer/string_write_bench.dart @@ -0,0 +1,321 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing ASCII strings (fast path) +class AsciiStringWriteBenchmark extends BenchmarkBase { + AsciiStringWriteBenchmark() : super('String write: ASCII only'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('Hello, World! This is a test string 123456789'); + } + writer.reset(); + } +} + +/// Benchmark for writing short ASCII strings +class ShortAsciiStringWriteBenchmark extends BenchmarkBase { + ShortAsciiStringWriteBenchmark() : super('String write: short ASCII'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 125; i++) { + writer + ..writeString('Hi') + ..writeString('Test') + ..writeString('Hello') + ..writeString('OK') + ..writeString('Error') + ..writeString('Success') + ..writeString('123') + ..writeString('ABC'); + } + writer.reset(); + } +} + +/// Benchmark for writing long ASCII strings +class LongAsciiStringWriteBenchmark extends BenchmarkBase { + LongAsciiStringWriteBenchmark() : super('String write: long ASCII'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + const longString = + 'The quick brown fox jumps over the lazy dog. ' + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' + 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.'; + for (var i = 0; i < 100; i++) { + writer.writeString(longString); + } + writer.reset(); + } +} + +/// Benchmark for writing Cyrillic strings (2-byte UTF-8) +class CyrillicStringWriteBenchmark extends BenchmarkBase { + CyrillicStringWriteBenchmark() + : super('String write: Cyrillic (2-byte UTF-8)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('Привет мир! Это тестовая строка на русском языке.'); + } + writer.reset(); + } +} + +/// Benchmark for writing CJK strings (3-byte UTF-8) +class CjkStringWriteBenchmark extends BenchmarkBase { + CjkStringWriteBenchmark() : super('String write: CJK (3-byte UTF-8)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('你好世界!这是一个测试字符串。日本語のテストも含まれています。'); + } + writer.reset(); + } +} + +/// Benchmark for writing emoji strings (4-byte UTF-8) +class EmojiStringWriteBenchmark extends BenchmarkBase { + EmojiStringWriteBenchmark() : super('String write: Emoji (4-byte UTF-8)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('🚀 🌍 🎉 👍 💻 🔥 ⚡ 🎯 🏆 💡 🌈 ✨ 🎨 🎭 🎪'); + } + writer.reset(); + } +} + +/// Benchmark for writing mixed Unicode strings +class MixedUnicodeStringWriteBenchmark extends BenchmarkBase { + MixedUnicodeStringWriteBenchmark() : super('String write: mixed Unicode'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeString('Hello мир 世界 🌍! Test тест 测试 🚀'); + } + writer.reset(); + } +} + +/// Benchmark for writing VarString (length-prefixed strings) +class VarStringAsciiWriteBenchmark extends BenchmarkBase { + VarStringAsciiWriteBenchmark() : super('VarString write: ASCII'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarString('Hello, World! This is a test string.'); + } + writer.reset(); + } +} + +/// Benchmark for writing VarString with mixed Unicode +class VarStringMixedWriteBenchmark extends BenchmarkBase { + VarStringMixedWriteBenchmark() : super('VarString write: mixed Unicode'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer.writeVarString('Hello мир 世界 🌍 Test тест 测试 🚀'); + } + writer.reset(); + } +} + +/// Benchmark for writing empty strings +class EmptyStringWriteBenchmark extends BenchmarkBase { + EmptyStringWriteBenchmark() : super('String write: empty strings'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 8192); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeString(''); + } + writer.reset(); + } +} + +/// Benchmark for realistic message protocol with strings +class RealisticMessageWriteBenchmark extends BenchmarkBase { + RealisticMessageWriteBenchmark() : super('String write: realistic message'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 100; i++) { + writer + ..writeString('user') + ..writeString('John Doe') + ..writeString('email') + ..writeString('john.doe@example.com') + ..writeString('message') + ..writeString('Hello 世界! 🌍') + ..writeString('timestamp') + ..writeString('2024-12-30T12:00:00Z') + ..writeString('locale') + ..writeString('ru-RU'); + } + writer.reset(); + } +} + +/// Benchmark for alternating short and long strings +class AlternatingStringWriteBenchmark extends BenchmarkBase { + AlternatingStringWriteBenchmark() + : super('String write: alternating lengths'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + const shortString = 'Hi'; + const longString = + 'This is a much longer string with more content to write and process'; + for (var i = 0; i < 500; i++) { + writer + ..writeString(shortString) + ..writeString(longString); + } + writer.reset(); + } +} + +/// Benchmark for writing very long strings (> 1KB) +class VeryLongStringWriteBenchmark extends BenchmarkBase { + VeryLongStringWriteBenchmark() : super('String write: very long (>1KB)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 65536); + } + + @override + void run() { + final longString = 'Lorem ipsum dolor sit amet. ' * 80; + for (var i = 0; i < 50; i++) { + writer.writeString(longString); + } + writer.reset(); + } +} + +void main() { + test('ASCII string benchmarks:', () { + AsciiStringWriteBenchmark().report(); + ShortAsciiStringWriteBenchmark().report(); + LongAsciiStringWriteBenchmark().report(); + EmptyStringWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('UTF-8 multi-byte benchmarks:', () { + CyrillicStringWriteBenchmark().report(); + CjkStringWriteBenchmark().report(); + EmojiStringWriteBenchmark().report(); + MixedUnicodeStringWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarString benchmarks:', () { + VarStringAsciiWriteBenchmark().report(); + VarStringMixedWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic string scenarios:', () { + RealisticMessageWriteBenchmark().report(); + AlternatingStringWriteBenchmark().report(); + VeryLongStringWriteBenchmark().report(); + }, tags: ['benchmark']); +} diff --git a/test/performance/writer/varint_write_bench.dart b/test/performance/writer/varint_write_bench.dart new file mode 100644 index 0000000..7b9314d --- /dev/null +++ b/test/performance/writer/varint_write_bench.dart @@ -0,0 +1,215 @@ +import 'package:benchmark_harness/benchmark_harness.dart'; +import 'package:pro_binary/pro_binary.dart'; +import 'package:test/test.dart'; + +/// Benchmark for writing VarUint in fast path (0-127) +class VarUintFastPathWriteBenchmark extends BenchmarkBase { + VarUintFastPathWriteBenchmark() : super('VarUint write: 0-127 (fast path)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(i % 128); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 2-byte values +class VarUint2ByteWriteBenchmark extends BenchmarkBase { + VarUint2ByteWriteBenchmark() : super('VarUint write: 128-16383 (2 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(128 + (i % 1000)); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 3-byte values +class VarUint3ByteWriteBenchmark extends BenchmarkBase { + VarUint3ByteWriteBenchmark() + : super('VarUint write: 16384-2097151 (3 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(16384 + (i % 10000)); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 4-byte values +class VarUint4ByteWriteBenchmark extends BenchmarkBase { + VarUint4ByteWriteBenchmark() + : super('VarUint write: 2097152-268435455 (4 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(2097152 + (i % 100000)); + } + writer.reset(); + } +} + +/// Benchmark for writing VarUint 5-byte values +class VarUint5ByteWriteBenchmark extends BenchmarkBase { + VarUint5ByteWriteBenchmark() : super('VarUint write: 268435456+ (5 bytes)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarUint(268435456 + i); + } + writer.reset(); + } +} + +/// Benchmark for writing positive VarInt (ZigZag encoded) +class VarIntPositiveWriteBenchmark extends BenchmarkBase { + VarIntPositiveWriteBenchmark() : super('VarInt write: positive (ZigZag)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(i); + } + writer.reset(); + } +} + +/// Benchmark for writing negative VarInt (ZigZag encoded) +class VarIntNegativeWriteBenchmark extends BenchmarkBase { + VarIntNegativeWriteBenchmark() : super('VarInt write: negative (ZigZag)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(-(i + 1)); + } + writer.reset(); + } +} + +/// Benchmark for writing mixed positive/negative VarInt +class VarIntMixedWriteBenchmark extends BenchmarkBase { + VarIntMixedWriteBenchmark() : super('VarInt write: mixed positive/negative'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 16384); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + writer.writeVarInt(i.isEven ? i : -i); + } + writer.reset(); + } +} + +/// Benchmark for realistic VarUint distribution +class VarUintMixedSizesWriteBenchmark extends BenchmarkBase { + VarUintMixedSizesWriteBenchmark() + : super('VarUint write: mixed sizes (realistic)'); + + late BinaryWriter writer; + + @override + void setup() { + writer = BinaryWriter(initialBufferSize: 32768); + } + + @override + void run() { + for (var i = 0; i < 1000; i++) { + final mod = i % 100; + if (mod < 70) { + writer.writeVarUint(i % 128); + } else if (mod < 90) { + writer.writeVarUint(128 + (i % 1000)); + } else if (mod < 98) { + writer.writeVarUint(16384 + (i % 10000)); + } else { + writer.writeVarUint(2097152 + i); + } + } + writer.reset(); + } +} + +void main() { + test('VarUint size benchmarks:', () { + VarUintFastPathWriteBenchmark().report(); + VarUint2ByteWriteBenchmark().report(); + VarUint3ByteWriteBenchmark().report(); + VarUint4ByteWriteBenchmark().report(); + VarUint5ByteWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('VarInt (ZigZag) benchmarks:', () { + VarIntPositiveWriteBenchmark().report(); + VarIntNegativeWriteBenchmark().report(); + VarIntMixedWriteBenchmark().report(); + }, tags: ['benchmark']); + + test('Realistic scenarios:', () { + VarUintMixedSizesWriteBenchmark().report(); + }, tags: ['benchmark']); +} From 0708983229fed40915d17c67d425057e254952ab Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 2 Jan 2026 11:45:24 +0200 Subject: [PATCH 21/29] wip --- dart_test.yaml | 5 -- lib/src/binary_writer.dart | 2 +- ...bench.dart => binary_read_bench_test.dart} | 84 ++++++++++++++----- ...ch.dart => fixed_int_read_bench_test.dart} | 0 ..._bench.dart => float_read_bench_test.dart} | 0 ..._bench.dart => navigation_bench_test.dart} | 0 ...bench.dart => string_read_bench_test.dart} | 0 ...bench.dart => varint_read_bench_test.dart} | 0 ...ench.dart => binary_write_bench_test.dart} | 79 ++++++++++++----- ...nch.dart => buffer_growth_bench_test.dart} | 0 ...h.dart => fixed_int_write_bench_test.dart} | 0 ...bench.dart => float_write_bench_test.dart} | 0 .../{pool_bench.dart => pool_bench_test.dart} | 0 ...ench.dart => string_write_bench_test.dart} | 0 ...ench.dart => varint_write_bench_test.dart} | 0 15 files changed, 123 insertions(+), 47 deletions(-) rename test/performance/reader/{binary_read_bench.dart => binary_read_bench_test.dart} (88%) rename test/performance/reader/{fixed_int_read_bench.dart => fixed_int_read_bench_test.dart} (100%) rename test/performance/reader/{float_read_bench.dart => float_read_bench_test.dart} (100%) rename test/performance/reader/{navigation_bench.dart => navigation_bench_test.dart} (100%) rename test/performance/reader/{string_read_bench.dart => string_read_bench_test.dart} (100%) rename test/performance/reader/{varint_read_bench.dart => varint_read_bench_test.dart} (100%) rename test/performance/writer/{binary_write_bench.dart => binary_write_bench_test.dart} (86%) rename test/performance/writer/{buffer_growth_bench.dart => buffer_growth_bench_test.dart} (100%) rename test/performance/writer/{fixed_int_write_bench.dart => fixed_int_write_bench_test.dart} (100%) rename test/performance/writer/{float_write_bench.dart => float_write_bench_test.dart} (100%) rename test/performance/writer/{pool_bench.dart => pool_bench_test.dart} (100%) rename test/performance/writer/{string_write_bench.dart => string_write_bench_test.dart} (100%) rename test/performance/writer/{varint_write_bench.dart => varint_write_bench_test.dart} (100%) diff --git a/dart_test.yaml b/dart_test.yaml index b0d1859..6c78dc3 100644 --- a/dart_test.yaml +++ b/dart_test.yaml @@ -5,8 +5,3 @@ tags: benchmark: description: Performance/benchmark tests (excluded from CI by default). - -# Include benchmark files in test discovery -# Note: By default, only *_test.dart files are discovered. -# This configuration allows *_bench.dart files to be found too. -filename: "*_{test,bench}.dart" diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 6ecab4d..16109b9 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -75,9 +75,9 @@ extension type BinaryWriter._(_WriterState _ws) { return; } + _ws.ensureSize(10); // Slow path: multi-byte VarInt final list = _ws.list; - _ws.ensureSize(10); // First byte (always has continuation bit) list[offset++] = (value & 0x7F) | 0x80; diff --git a/test/performance/reader/binary_read_bench.dart b/test/performance/reader/binary_read_bench_test.dart similarity index 88% rename from test/performance/reader/binary_read_bench.dart rename to test/performance/reader/binary_read_bench_test.dart index 43f6dfd..bdbc171 100644 --- a/test/performance/reader/binary_read_bench.dart +++ b/test/performance/reader/binary_read_bench_test.dart @@ -15,7 +15,7 @@ class SmallBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); // Write 1000 small byte arrays @@ -26,6 +26,9 @@ class SmallBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -44,7 +47,7 @@ class MediumBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(64, (i) => i % 256)); // Write 1000 medium byte arrays @@ -55,6 +58,9 @@ class MediumBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -77,16 +83,19 @@ class LargeBytesReadBenchmark extends BenchmarkBase { final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); // Write 100 large byte arrays - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readBytes(1024); } reader.reset(); @@ -102,7 +111,7 @@ class VeryLargeBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 64 * 1024 * 10); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); // Write 10 very large byte arrays @@ -113,6 +122,9 @@ class VeryLargeBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 10; i++) { @@ -131,7 +143,7 @@ class VarBytesSmallReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); // Write 1000 VarBytes @@ -142,6 +154,9 @@ class VarBytesSmallReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -160,7 +175,7 @@ class VarBytesMediumReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 256 * 1024); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); // Write 500 VarBytes @@ -171,6 +186,9 @@ class VarBytesMediumReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 500; i++) { @@ -189,7 +207,7 @@ class VarBytesLargeReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 512 * 1024); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); // Write 100 VarBytes @@ -200,6 +218,9 @@ class VarBytesLargeReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -218,7 +239,7 @@ class EmptyBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 empty byte arrays for (var i = 0; i < 1000; i++) { @@ -228,6 +249,9 @@ class EmptyBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -246,7 +270,7 @@ class PeekBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(16, (i) => i)); writer.writeBytes(data); @@ -254,6 +278,9 @@ class PeekBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -272,7 +299,7 @@ class ReadRemainingBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); // Write 100 chunks @@ -283,6 +310,9 @@ class ReadRemainingBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -303,13 +333,13 @@ class MixedBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); // Simulate a protocol message: // - Header (16 bytes) // - Payload (variable: 64, 128, 256 bytes) // - Checksum (4 bytes) - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { final header = Uint8List.fromList(List.generate(16, (j) => j)); final payload = Uint8List.fromList( List.generate(64 + (i % 3) * 64, (j) => (j + i) % 256), @@ -325,9 +355,12 @@ class MixedBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader ..readBytes(16) // Header ..readBytes(64 + (i % 3) * 64) // Payload @@ -346,12 +379,12 @@ class AlternatingBytesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); final small = Uint8List.fromList([1, 2, 3, 4]); final large = Uint8List.fromList(List.generate(512, (i) => i % 256)); // Alternate between small and large - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer ..writeBytes(small) ..writeBytes(large); @@ -360,9 +393,12 @@ class AlternatingBytesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader ..readBytes(4) ..readBytes(512); @@ -383,19 +419,22 @@ class SequentialSmallReadsReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); // Write 4000 bytes as 1-byte chunks - for (var i = 0; i < 4000; i++) { + for (var i = 0; i < 1000; i++) { writer.writeUint8(i % 256); } buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 4000; i++) { + for (var i = 0; i < 1000; i++) { reader.readBytes(1); } reader.reset(); @@ -411,7 +450,7 @@ class SkipAndReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); // Write pattern: 8 bytes data, 8 bytes padding for (var i = 0; i < 1000; i++) { @@ -425,6 +464,9 @@ class SkipAndReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { diff --git a/test/performance/reader/fixed_int_read_bench.dart b/test/performance/reader/fixed_int_read_bench_test.dart similarity index 100% rename from test/performance/reader/fixed_int_read_bench.dart rename to test/performance/reader/fixed_int_read_bench_test.dart diff --git a/test/performance/reader/float_read_bench.dart b/test/performance/reader/float_read_bench_test.dart similarity index 100% rename from test/performance/reader/float_read_bench.dart rename to test/performance/reader/float_read_bench_test.dart diff --git a/test/performance/reader/navigation_bench.dart b/test/performance/reader/navigation_bench_test.dart similarity index 100% rename from test/performance/reader/navigation_bench.dart rename to test/performance/reader/navigation_bench_test.dart diff --git a/test/performance/reader/string_read_bench.dart b/test/performance/reader/string_read_bench_test.dart similarity index 100% rename from test/performance/reader/string_read_bench.dart rename to test/performance/reader/string_read_bench_test.dart diff --git a/test/performance/reader/varint_read_bench.dart b/test/performance/reader/varint_read_bench_test.dart similarity index 100% rename from test/performance/reader/varint_read_bench.dart rename to test/performance/reader/varint_read_bench_test.dart diff --git a/test/performance/writer/binary_write_bench.dart b/test/performance/writer/binary_write_bench_test.dart similarity index 86% rename from test/performance/writer/binary_write_bench.dart rename to test/performance/writer/binary_write_bench_test.dart index a9bb0a8..8f78fe2 100644 --- a/test/performance/writer/binary_write_bench.dart +++ b/test/performance/writer/binary_write_bench_test.dart @@ -13,10 +13,13 @@ class SmallBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 16384); + writer = BinaryWriter(); data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -35,10 +38,13 @@ class MediumBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 65536); + writer = BinaryWriter(); data = Uint8List.fromList(List.generate(64, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -57,13 +63,16 @@ class LargeBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 1024 * 1024); + writer = BinaryWriter(); data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } writer.reset(); @@ -79,13 +88,16 @@ class VeryLargeBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 64 * 1024 * 10); + writer = BinaryWriter(); data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 10; i++) { + for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } writer.reset(); @@ -101,10 +113,13 @@ class VarBytesSmallWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 16384); + writer = BinaryWriter(); data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -123,13 +138,16 @@ class VarBytesMediumWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 256 * 1024); + writer = BinaryWriter(); data = Uint8List.fromList(List.generate(256, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { writer.writeVarBytes(data); } writer.reset(); @@ -145,13 +163,16 @@ class VarBytesLargeWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 512 * 1024); + writer = BinaryWriter(); data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer.writeVarBytes(data); } writer.reset(); @@ -166,9 +187,12 @@ class EmptyBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 8192); + writer = BinaryWriter(); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -189,7 +213,7 @@ class MixedBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 65536); + writer = BinaryWriter(); header = Uint8List.fromList(List.generate(16, (j) => j)); payloads = [ Uint8List.fromList(List.generate(64, (j) => j % 256)), @@ -199,9 +223,12 @@ class MixedBytesWriteBenchmark extends BenchmarkBase { checksum = Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF]); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer ..writeBytes(header) ..writeBytes(payloads[i % 3]) @@ -221,14 +248,17 @@ class AlternatingBytesWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 65536); + writer = BinaryWriter(); small = Uint8List.fromList([1, 2, 3, 4]); large = Uint8List.fromList(List.generate(512, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer ..writeBytes(small) ..writeBytes(large); @@ -246,12 +276,15 @@ class SequentialSmallWritesBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 16384); + writer = BinaryWriter(); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 4000; i++) { + for (var i = 0; i < 1000; i++) { writer.writeUint8(i % 256); } writer.reset(); @@ -267,10 +300,13 @@ class ListIntWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 65536); + writer = BinaryWriter(); data = List.generate(64, (i) => i % 256); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -290,11 +326,14 @@ class Uint8ListViewWriteBenchmark extends BenchmarkBase { @override void setup() { - writer = BinaryWriter(initialBufferSize: 65536); + writer = BinaryWriter(); data = Uint8List.fromList(List.generate(128, (i) => i % 256)); view = Uint8List.view(data.buffer, 32, 64); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { diff --git a/test/performance/writer/buffer_growth_bench.dart b/test/performance/writer/buffer_growth_bench_test.dart similarity index 100% rename from test/performance/writer/buffer_growth_bench.dart rename to test/performance/writer/buffer_growth_bench_test.dart diff --git a/test/performance/writer/fixed_int_write_bench.dart b/test/performance/writer/fixed_int_write_bench_test.dart similarity index 100% rename from test/performance/writer/fixed_int_write_bench.dart rename to test/performance/writer/fixed_int_write_bench_test.dart diff --git a/test/performance/writer/float_write_bench.dart b/test/performance/writer/float_write_bench_test.dart similarity index 100% rename from test/performance/writer/float_write_bench.dart rename to test/performance/writer/float_write_bench_test.dart diff --git a/test/performance/writer/pool_bench.dart b/test/performance/writer/pool_bench_test.dart similarity index 100% rename from test/performance/writer/pool_bench.dart rename to test/performance/writer/pool_bench_test.dart diff --git a/test/performance/writer/string_write_bench.dart b/test/performance/writer/string_write_bench_test.dart similarity index 100% rename from test/performance/writer/string_write_bench.dart rename to test/performance/writer/string_write_bench_test.dart diff --git a/test/performance/writer/varint_write_bench.dart b/test/performance/writer/varint_write_bench_test.dart similarity index 100% rename from test/performance/writer/varint_write_bench.dart rename to test/performance/writer/varint_write_bench_test.dart From e6f462fe5d0a7df8f78704d4507ed088c515af05 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Fri, 2 Jan 2026 12:10:19 +0200 Subject: [PATCH 22/29] Refactor benchmarks to remove initial buffer size and increase test iterations - Updated all benchmark tests to use a default BinaryWriter without specifying an initial buffer size. - Increased the number of iterations for various read benchmarks from 100 to 1000 to enhance performance testing. - Added exercise methods to all benchmarks for consistency and clarity in execution. --- .../reader/binary_read_bench_test.dart | 39 +++-- .../reader/fixed_int_read_bench_test.dart | 103 +++++++++++--- .../reader/float_read_bench_test.dart | 100 +++++++++---- .../reader/navigation_bench_test.dart | 91 +++++++++--- .../reader/string_read_bench_test.dart | 133 ++++++++++++------ .../reader/varint_read_bench_test.dart | 30 ++++ 6 files changed, 360 insertions(+), 136 deletions(-) diff --git a/test/performance/reader/binary_read_bench_test.dart b/test/performance/reader/binary_read_bench_test.dart index bdbc171..61c3924 100644 --- a/test/performance/reader/binary_read_bench_test.dart +++ b/test/performance/reader/binary_read_bench_test.dart @@ -18,10 +18,10 @@ class SmallBytesReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - // Write 1000 small byte arrays for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -50,10 +50,10 @@ class MediumBytesReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(64, (i) => i % 256)); - // Write 1000 medium byte arrays for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -82,10 +82,10 @@ class LargeBytesReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(initialBufferSize: 1024 * 1024); final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); - // Write 100 large byte arrays for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -114,10 +114,10 @@ class VeryLargeBytesReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(64 * 1024, (i) => i % 256)); - // Write 10 very large byte arrays - for (var i = 0; i < 10; i++) { + for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -146,10 +146,10 @@ class VarBytesSmallReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); - // Write 1000 VarBytes for (var i = 0; i < 1000; i++) { writer.writeVarBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -178,10 +178,10 @@ class VarBytesMediumReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); - // Write 500 VarBytes - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { writer.writeVarBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -191,7 +191,7 @@ class VarBytesMediumReadBenchmark extends BenchmarkBase { @override void run() { - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { reader.readVarBytes(); } reader.reset(); @@ -210,10 +210,10 @@ class VarBytesLargeReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); - // Write 100 VarBytes - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer.writeVarBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -223,7 +223,7 @@ class VarBytesLargeReadBenchmark extends BenchmarkBase { @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readVarBytes(); } reader.reset(); @@ -240,8 +240,6 @@ class EmptyBytesReadBenchmark extends BenchmarkBase { @override void setup() { final writer = BinaryWriter(); - - // Write 1000 empty byte arrays for (var i = 0; i < 1000; i++) { writer.writeBytes([]); } @@ -302,10 +300,10 @@ class ReadRemainingBytesReadBenchmark extends BenchmarkBase { final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(1024, (i) => i % 256)); - // Write 100 chunks - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -315,7 +313,7 @@ class ReadRemainingBytesReadBenchmark extends BenchmarkBase { @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readBytes(1024); } reader.reset(); @@ -351,6 +349,7 @@ class MixedBytesReadBenchmark extends BenchmarkBase { ..writeBytes(payload) ..writeBytes(checksum); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -383,12 +382,12 @@ class AlternatingBytesReadBenchmark extends BenchmarkBase { final small = Uint8List.fromList([1, 2, 3, 4]); final large = Uint8List.fromList(List.generate(512, (i) => i % 256)); - // Alternate between small and large for (var i = 0; i < 1000; i++) { writer ..writeBytes(small) ..writeBytes(large); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -421,10 +420,10 @@ class SequentialSmallReadsReadBenchmark extends BenchmarkBase { void setup() { final writer = BinaryWriter(); - // Write 4000 bytes as 1-byte chunks for (var i = 0; i < 1000; i++) { writer.writeUint8(i % 256); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } @@ -452,7 +451,6 @@ class SkipAndReadBenchmark extends BenchmarkBase { void setup() { final writer = BinaryWriter(); - // Write pattern: 8 bytes data, 8 bytes padding for (var i = 0; i < 1000; i++) { final data = Uint8List.fromList(List.generate(8, (j) => (i + j) % 256)); final padding = Uint8List.fromList(List.generate(8, (_) => 0)); @@ -460,6 +458,7 @@ class SkipAndReadBenchmark extends BenchmarkBase { ..writeBytes(data) ..writeBytes(padding); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } diff --git a/test/performance/reader/fixed_int_read_bench_test.dart b/test/performance/reader/fixed_int_read_bench_test.dart index 0848d44..6838728 100644 --- a/test/performance/reader/fixed_int_read_bench_test.dart +++ b/test/performance/reader/fixed_int_read_bench_test.dart @@ -16,15 +16,19 @@ class Uint8ReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Uint8 values + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeUint8(i % 256); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -43,15 +47,19 @@ class Int8ReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Int8 values + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeInt8((i % 256) - 128); // Range: -128 to 127 } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -70,15 +78,19 @@ class Uint16BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Uint16 values + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeUint16((i * 257) % 65536); // Varied values } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -97,15 +109,19 @@ class Uint16LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Uint16 values in little-endian + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeUint16((i * 257) % 65536, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -124,15 +140,19 @@ class Int16BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Int16 values + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeInt16((i * 257) % 65536 - 32768); // Range: -32768 to 32767 } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -151,15 +171,19 @@ class Int16LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Int16 values in little-endian + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeInt16((i * 257) % 65536 - 32768, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -178,7 +202,7 @@ class Uint32BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Uint32 values for (var i = 0; i < 1000; i++) { writer.writeUint32((i * 1000000 + i * 123) % 4294967296); @@ -187,6 +211,9 @@ class Uint32BigEndianReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -205,15 +232,19 @@ class Uint32LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Uint32 values in little-endian + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeUint32((i * 1000000 + i * 123) % 4294967296, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -232,15 +263,19 @@ class Int32BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Int32 values for (var i = 0; i < 1000; i++) { writer.writeInt32((i * 1000000 + i * 123) % 4294967296 - 2147483648); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -259,18 +294,22 @@ class Int32LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Int32 values in little-endian + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeInt32( (i * 1000000 + i * 123) % 4294967296 - 2147483648, .little, ); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -289,7 +328,7 @@ class Uint64BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Uint64 values for (var i = 0; i < 1000; i++) { writer.writeUint64(i * 1000000000 + i * 12345); @@ -298,6 +337,9 @@ class Uint64BigEndianReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -316,15 +358,19 @@ class Uint64LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Uint64 values in little-endian for (var i = 0; i < 1000; i++) { writer.writeUint64(i * 1000000000 + i * 12345, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -343,7 +389,7 @@ class Int64BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Int64 values for (var i = 0; i < 1000; i++) { final value = i.isEven @@ -351,10 +397,14 @@ class Int64BigEndianReadBenchmark extends BenchmarkBase { : -(i * 1000000000 + i * 12345); writer.writeInt64(value); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -373,7 +423,7 @@ class Int64LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Int64 values in little-endian for (var i = 0; i < 1000; i++) { final value = i.isEven @@ -381,10 +431,14 @@ class Int64LittleEndianReadBenchmark extends BenchmarkBase { : -(i * 1000000000 + i * 12345); writer.writeInt64(value, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -407,7 +461,6 @@ class MixedFixedIntReadBenchmark extends BenchmarkBase { @override void setup() { final writer = BinaryWriter(initialBufferSize: 8192); - // Write mixed integer types as they might appear in a real protocol for (var i = 0; i < 1000; i++) { writer ..writeUint8(127) // Message type @@ -419,10 +472,14 @@ class MixedFixedIntReadBenchmark extends BenchmarkBase { ..writeInt16(-1000, .little) // Medium signed value ..writeInt64(-10000000, .little); // Large signed value } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { diff --git a/test/performance/reader/float_read_bench_test.dart b/test/performance/reader/float_read_bench_test.dart index c2b9e94..69f187b 100644 --- a/test/performance/reader/float_read_bench_test.dart +++ b/test/performance/reader/float_read_bench_test.dart @@ -16,16 +16,19 @@ class Float32BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Float32 values with varied magnitudes + final writer = BinaryWriter(); for (var i = 0; i < 1000; i++) { final value = (i * 3.14159) - 500.0; writer.writeFloat32(value); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -44,16 +47,20 @@ class Float32LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 Float32 values in little-endian for (var i = 0; i < 1000; i++) { final value = (i * 3.14159) - 500.0; writer.writeFloat32(value, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -75,16 +82,20 @@ class Float64BigEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Float64 values with varied magnitudes + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { final value = (i * 2.718281828) - 1000.0; writer.writeFloat64(value); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -103,16 +114,20 @@ class Float64LittleEndianReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write 1000 Float64 values in little-endian + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { final value = (i * 2.718281828) - 1000.0; writer.writeFloat64(value, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -134,20 +149,24 @@ class Float32SpecialValuesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write special values: NaN, Infinity, -Infinity, -0.0, normal values + final writer = BinaryWriter(); + for (var i = 0; i < 200; i++) { writer - ..writeFloat32(double.nan, .little) - ..writeFloat32(double.infinity, .little) - ..writeFloat32(double.negativeInfinity, .little) + ..writeFloat32(.nan, .little) + ..writeFloat32(.infinity, .little) + ..writeFloat32(.negativeInfinity, .little) ..writeFloat32(-0, .little) ..writeFloat32(1, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -166,20 +185,24 @@ class Float64SpecialValuesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write special values: NaN, Infinity, -Infinity, -0.0, normal values + final writer = BinaryWriter(); + for (var i = 0; i < 200; i++) { writer - ..writeFloat64(double.nan, .little) - ..writeFloat64(double.infinity, .little) - ..writeFloat64(double.negativeInfinity, .little) + ..writeFloat64(.nan, .little) + ..writeFloat64(.infinity, .little) + ..writeFloat64(.negativeInfinity, .little) ..writeFloat64(-0, .little) ..writeFloat64(1, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -201,15 +224,19 @@ class Float32SmallValuesReadBenchmark extends BenchmarkBase { @override void setup() { final writer = BinaryWriter(initialBufferSize: 8192); - // Write very small values near the subnormal range + for (var i = 0; i < 1000; i++) { final value = (i + 1) * 1e-38; // Near Float32 min positive normal writer.writeFloat32(value, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -228,16 +255,20 @@ class Float64SmallValuesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write very small values near the subnormal range + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { final value = (i + 1) * 1e-308; // Near Float64 min positive normal writer.writeFloat64(value, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -256,8 +287,8 @@ class Float32LargeValuesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write large values near Float32 max + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { final value = (i + 1) * 1e35; // Near Float32 max (~3.4e38) writer.writeFloat32(value, .little); @@ -266,6 +297,9 @@ class Float32LargeValuesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -284,8 +318,8 @@ class Float64LargeValuesReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write large values near Float64 max + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { final value = (i + 1) * 1e305; // Near Float64 max (~1.8e308) writer.writeFloat64(value, .little); @@ -294,6 +328,9 @@ class Float64LargeValuesReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -315,8 +352,8 @@ class MixedFloatReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Write mixed Float32/Float64 as in a typical game or graphics protocol + final writer = BinaryWriter(); + for (var i = 0; i < 100; i++) { writer // 3D position (Float32 x3) @@ -336,10 +373,14 @@ class MixedFloatReadBenchmark extends BenchmarkBase { ..writeFloat32(0.2, .little) ..writeFloat32(1, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -376,17 +417,20 @@ class AlternatingFloatReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); - // Alternate between Float32 and Float64 + final writer = BinaryWriter(); for (var i = 0; i < 500; i++) { writer ..writeFloat32(i * 3.14, .little) ..writeFloat64(i * 2.718, .little); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 500; i++) { diff --git a/test/performance/reader/navigation_bench_test.dart b/test/performance/reader/navigation_bench_test.dart index 2668af0..9364117 100644 --- a/test/performance/reader/navigation_bench_test.dart +++ b/test/performance/reader/navigation_bench_test.dart @@ -16,15 +16,19 @@ class SkipSmallOffsetBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); - // Write 1000 chunks of 8 bytes each + final writer = BinaryWriter(); + for (var i = 0; i < 1000; i++) { writer.writeUint64(i); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -43,16 +47,20 @@ class SkipMediumOffsetBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 256 * 1024); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(256, (i) => i % 256)); - // Write 1000 chunks of 256 bytes + for (var i = 0; i < 1000; i++) { writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -71,7 +79,7 @@ class SkipLargeOffsetBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 4 * 1024 * 1024); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(4096, (i) => i % 256)); // Write 1000 chunks of 4KB for (var i = 0; i < 1000; i++) { @@ -81,6 +89,9 @@ class SkipLargeOffsetBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -101,14 +112,18 @@ class SeekForwardBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 100000); + final writer = BinaryWriter(); // Write 100KB of data final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); writer.writeBytes(data); + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { // Seek to 1000 different positions @@ -128,14 +143,19 @@ class SeekBackwardBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 100000); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); writer.writeBytes(data); + buffer = writer.takeBytes(); reader = BinaryReader(buffer); + reader.seek(90000); // Start near end } + @override + void exercise() => run(); + @override void run() { // Seek backward to 1000 different positions @@ -156,16 +176,20 @@ class SeekRandomAccessBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 100000); + final writer = BinaryWriter(); final data = Uint8List.fromList(List.generate(100000, (i) => i % 256)); + writer.writeBytes(data); buffer = writer.takeBytes(); - reader = BinaryReader(buffer); + reader = BinaryReader(buffer); // Pre-calculate random-like positions (deterministic for consistency) positions = List.generate(1000, (i) => (i * 7919) % 90000); } + @override + void exercise() => run(); + @override void run() { // Disable lint for using for-in to emphasize the benchmark nature @@ -188,14 +212,18 @@ class RewindBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); for (var i = 0; i < 1000; i++) { writer.writeUint64(i); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -217,14 +245,18 @@ class ResetBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); for (var i = 0; i < 1000; i++) { writer.writeUint64(i); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -247,7 +279,7 @@ class GetPositionBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); for (var i = 0; i < 1000; i++) { writer.writeUint64(i); } @@ -255,6 +287,9 @@ class GetPositionBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -273,14 +308,18 @@ class RemainingBytesBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); for (var i = 0; i < 1000; i++) { writer.writeUint64(i); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -306,9 +345,9 @@ class RealisticParsingNavigationBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 32768); + final writer = BinaryWriter(); // Write protocol-like data: header (4 bytes) + payload (variable) - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { final payloadSize = 16 + (i % 8) * 8; writer ..writeUint32(payloadSize) // Header with payload size @@ -318,9 +357,12 @@ class RealisticParsingNavigationBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { // 1. Get current position reader.offset; // 2. Peek at header to determine payload size @@ -352,7 +394,7 @@ class SeekAndReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); // Write 100 records of 64 bytes each offsets = []; for (var i = 0; i < 100; i++) { @@ -360,10 +402,14 @@ class SeekAndReadBenchmark extends BenchmarkBase { final data = Uint8List.fromList(List.generate(64, (j) => (i + j) % 256)); writer.writeBytes(data); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { // Read records in non-sequential order @@ -388,17 +434,21 @@ class SkipAndPeekBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); // Write pattern: 4 bytes to skip, 4 bytes to peek for (var i = 0; i < 1000; i++) { writer ..writeUint32(0xDEADBEEF) // Skip this ..writeUint32(i); // Peek at this } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -422,7 +472,7 @@ class BacktrackNavigationBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); for (var i = 0; i < 2000; i++) { writer.writeUint32(i); } @@ -430,6 +480,9 @@ class BacktrackNavigationBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 500; i++) { diff --git a/test/performance/reader/string_read_bench_test.dart b/test/performance/reader/string_read_bench_test.dart index 063c69c..e54ee4a 100644 --- a/test/performance/reader/string_read_bench_test.dart +++ b/test/performance/reader/string_read_bench_test.dart @@ -17,21 +17,24 @@ class AsciiStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const asciiString = 'Hello, World! This is a test string 123456789'; stringLength = asciiString.length; - // Write 100 ASCII strings - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { writer.writeString(asciiString); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(stringLength); } reader.reset(); @@ -47,7 +50,7 @@ class ShortAsciiStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const strings = [ 'Hi', 'Test', @@ -60,7 +63,7 @@ class ShortAsciiStringReadBenchmark extends BenchmarkBase { ]; // Write 1000 short strings - for (var i = 0; i < 125; i++) { + for (var i = 0; i < 1000; i++) { strings.forEach(writer.writeString); } buffer = writer.takeBytes(); @@ -70,7 +73,7 @@ class ShortAsciiStringReadBenchmark extends BenchmarkBase { @override void run() { // Read in same pattern - for (var i = 0; i < 125; i++) { + for (var i = 0; i < 1000; i++) { reader ..readString(2) // Hi ..readString(4) // Test @@ -95,7 +98,7 @@ class LongAsciiStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 32768); + final writer = BinaryWriter(); const longString = 'The quick brown fox jumps over the lazy dog. ' 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ' @@ -103,17 +106,20 @@ class LongAsciiStringReadBenchmark extends BenchmarkBase { 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.'; stringLength = longString.length; - // Write 100 long ASCII strings - for (var i = 0; i < 100; i++) { + // Write 1000 long ASCII strings + for (var i = 0; i < 1000; i++) { writer.writeString(longString); } buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(stringLength); } reader.reset(); @@ -130,21 +136,24 @@ class CyrillicStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const cyrillicString = 'Привет мир! Это тестовая строка на русском языке.'; byteLength = getUtf8Length(cyrillicString); - // Write 100 Cyrillic strings - for (var i = 0; i < 100; i++) { + // Write 1000 Cyrillic strings + for (var i = 0; i < 1000; i++) { writer.writeString(cyrillicString); } buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(byteLength); } reader.reset(); @@ -161,21 +170,24 @@ class CjkStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const cjkString = '你好世界!这是一个测试字符串。日本語のテストも含まれています。'; byteLength = getUtf8Length(cjkString); - // Write 100 CJK strings - for (var i = 0; i < 100; i++) { + // Write 1000 CJK strings + for (var i = 0; i < 1000; i++) { writer.writeString(cjkString); } buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(byteLength); } reader.reset(); @@ -196,17 +208,21 @@ class EmojiStringReadBenchmark extends BenchmarkBase { const emojiString = '🚀 🌍 🎉 👍 💻 🔥 ⚡ 🎯 🏆 💡 🌈 ✨ 🎨 🎭 🎪'; byteLength = getUtf8Length(emojiString); - // Write 100 emoji strings - for (var i = 0; i < 100; i++) { + // Write 1000 emoji strings + for (var i = 0; i < 1000; i++) { writer.writeString(emojiString); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(byteLength); } reader.reset(); @@ -226,21 +242,24 @@ class MixedUnicodeStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const mixedString = 'Hello мир 世界 🌍! Test тест 测试 🚀'; byteLength = getUtf8Length(mixedString); - // Write 100 mixed strings - for (var i = 0; i < 100; i++) { + // Write 1000 mixed strings + for (var i = 0; i < 1000; i++) { writer.writeString(mixedString); } buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(byteLength); } reader.reset(); @@ -256,20 +275,24 @@ class VarStringAsciiReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const asciiString = 'Hello, World! This is a test string.'; - // Write 100 VarStrings - for (var i = 0; i < 100; i++) { + // Write 1000 VarStrings + for (var i = 0; i < 1000; i++) { writer.writeVarString(asciiString); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readVarString(); } reader.reset(); @@ -285,20 +308,24 @@ class VarStringMixedReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 16384); + final writer = BinaryWriter(); const mixedString = 'Hello мир 世界 🌍 Test тест 测试 🚀'; - // Write 100 VarStrings - for (var i = 0; i < 100; i++) { + // Write 1000 VarStrings + for (var i = 0; i < 1000; i++) { writer.writeVarString(mixedString); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { reader.readVarString(); } reader.reset(); @@ -314,7 +341,7 @@ class EmptyStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 8192); + final writer = BinaryWriter(); // Write 1000 empty strings for (var i = 0; i < 1000; i++) { @@ -324,6 +351,9 @@ class EmptyStringReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -346,7 +376,7 @@ class RealisticMessageReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 32768); + final writer = BinaryWriter(); // Typical message fields const fields = [ @@ -364,17 +394,21 @@ class RealisticMessageReadBenchmark extends BenchmarkBase { fieldLengths = fields.map(getUtf8Length).toList(); - // Write 100 messages - for (var i = 0; i < 100; i++) { + // Write 1000 messages + for (var i = 0; i < 1000; i++) { fields.forEach(writer.writeString); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 100; i++) { + for (var i = 0; i < 1000; i++) { fieldLengths.forEach(reader.readString); } reader.reset(); @@ -392,7 +426,7 @@ class AlternatingStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 32768); + final writer = BinaryWriter(); const shortString = 'Hi'; const longString = 'This is a much longer string with more content to read and process'; @@ -401,7 +435,7 @@ class AlternatingStringReadBenchmark extends BenchmarkBase { longLength = longString.length; // Alternate between short and long strings - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { writer ..writeString(shortString) ..writeString(longString); @@ -410,9 +444,12 @@ class AlternatingStringReadBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 500; i++) { + for (var i = 0; i < 1000; i++) { reader ..readString(shortLength) ..readString(longLength); @@ -431,22 +468,26 @@ class VeryLongStringReadBenchmark extends BenchmarkBase { @override void setup() { - final writer = BinaryWriter(initialBufferSize: 65536); + final writer = BinaryWriter(); // Create a ~2KB string final longString = 'Lorem ipsum dolor sit amet. ' * 80; stringLength = longString.length; - // Write 50 very long strings - for (var i = 0; i < 50; i++) { + // Write 1000 very long strings + for (var i = 0; i < 1000; i++) { writer.writeString(longString); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { - for (var i = 0; i < 50; i++) { + for (var i = 0; i < 1000; i++) { reader.readString(stringLength); } reader.reset(); diff --git a/test/performance/reader/varint_read_bench_test.dart b/test/performance/reader/varint_read_bench_test.dart index 138c42c..c5a4193 100644 --- a/test/performance/reader/varint_read_bench_test.dart +++ b/test/performance/reader/varint_read_bench_test.dart @@ -22,10 +22,14 @@ class VarUintFastPathBenchmark extends BenchmarkBase { for (var i = 0; i < 1000; i++) { writer.writeVarUint(i % 128); // Values 0-127 } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -53,10 +57,14 @@ class VarUint2ByteBenchmark extends BenchmarkBase { for (var i = 0; i < 1000; i++) { writer.writeVarUint(128 + (i % 100)); // Values 128-227 } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -81,10 +89,14 @@ class VarUint3ByteBenchmark extends BenchmarkBase { for (var i = 0; i < 1000; i++) { writer.writeVarUint(16384 + (i % 1000) * 100); } + buffer = writer.takeBytes(); reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -114,6 +126,9 @@ class VarUint4ByteBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -146,6 +161,9 @@ class VarUint5ByteBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -178,6 +196,9 @@ class VarIntPositiveBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -209,6 +230,9 @@ class VarIntNegativeBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -241,6 +265,9 @@ class VarIntMixedBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -287,6 +314,9 @@ class VarUintMixedSizesBenchmark extends BenchmarkBase { reader = BinaryReader(buffer); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { From bd52597185da6eb3cac77040d3af7ad8cdc452bb Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 6 Jan 2026 12:02:25 +0200 Subject: [PATCH 23/29] Refactor float handling in tests and benchmarks to use shorthand notation for special values --- test/integration/integration_test.dart | 491 +++++++++++++++++- .../writer/buffer_growth_bench_test.dart | 4 +- .../writer/fixed_int_write_bench_test.dart | 26 +- .../writer/float_write_bench_test.dart | 58 +-- test/unit/binary_reader_test.dart | 12 +- test/unit/binary_writer_test.dart | 12 +- 6 files changed, 533 insertions(+), 70 deletions(-) diff --git a/test/integration/integration_test.dart b/test/integration/integration_test.dart index aaa7fa8..5622db8 100644 --- a/test/integration/integration_test.dart +++ b/test/integration/integration_test.dart @@ -446,7 +446,7 @@ void main() { group('Float special values', () { test('write and read Float32 NaN', () { - final writer = BinaryWriter()..writeFloat32(double.nan); + final writer = BinaryWriter()..writeFloat32(.nan); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -454,7 +454,7 @@ void main() { }); test('write and read Float32 positive Infinity', () { - final writer = BinaryWriter()..writeFloat32(double.infinity); + final writer = BinaryWriter()..writeFloat32(.infinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -462,7 +462,7 @@ void main() { }); test('write and read Float32 negative Infinity', () { - final writer = BinaryWriter()..writeFloat32(double.negativeInfinity); + final writer = BinaryWriter()..writeFloat32(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -470,7 +470,7 @@ void main() { }); test('write and read Float64 NaN', () { - final writer = BinaryWriter()..writeFloat64(double.nan); + final writer = BinaryWriter()..writeFloat64(.nan); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -478,7 +478,7 @@ void main() { }); test('write and read Float64 positive Infinity', () { - final writer = BinaryWriter()..writeFloat64(double.infinity); + final writer = BinaryWriter()..writeFloat64(.infinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -486,7 +486,7 @@ void main() { }); test('write and read Float64 negative Infinity', () { - final writer = BinaryWriter()..writeFloat64(double.negativeInfinity); + final writer = BinaryWriter()..writeFloat64(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -507,12 +507,12 @@ void main() { 'write and read multiple special float values together', () { final writer = BinaryWriter() - ..writeFloat32(double.nan) - ..writeFloat32(double.infinity) - ..writeFloat32(double.negativeInfinity) - ..writeFloat64(double.nan) - ..writeFloat64(double.infinity) - ..writeFloat64(double.negativeInfinity); + ..writeFloat32(.nan) + ..writeFloat32(.infinity) + ..writeFloat32(.negativeInfinity) + ..writeFloat64(.nan) + ..writeFloat64(.infinity) + ..writeFloat64(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -921,5 +921,472 @@ void main() { } }); }); + + group('Variable-length integer operations', () { + test('write and read VarUint single byte', () { + final writer = BinaryWriter()..writeVarUint(127); + final bytes = writer.takeBytes(); + + expect(bytes.length, equals(1)); + + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(127)); + }); + + test('write and read VarUint two bytes', () { + final writer = BinaryWriter()..writeVarUint(300); + final bytes = writer.takeBytes(); + + expect(bytes.length, equals(2)); + + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(300)); + }); + + test('write and read VarUint large value', () { + final writer = BinaryWriter()..writeVarUint(1000000); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(1000000)); + }); + + test('write and read multiple VarUints', () { + final writer = BinaryWriter() + ..writeVarUint(0) + ..writeVarUint(127) + ..writeVarUint(128) + ..writeVarUint(16383) + ..writeVarUint(16384) + ..writeVarUint(2097151); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(0)); + expect(reader.readVarUint(), equals(127)); + expect(reader.readVarUint(), equals(128)); + expect(reader.readVarUint(), equals(16383)); + expect(reader.readVarUint(), equals(16384)); + expect(reader.readVarUint(), equals(2097151)); + }); + + test('write and read VarInt positive values', () { + final writer = BinaryWriter() + ..writeVarInt(0) + ..writeVarInt(42) + ..writeVarInt(1000); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarInt(), equals(0)); + expect(reader.readVarInt(), equals(42)); + expect(reader.readVarInt(), equals(1000)); + }); + + test('write and read VarInt negative values', () { + final writer = BinaryWriter() + ..writeVarInt(-1) + ..writeVarInt(-64) + ..writeVarInt(-1000); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarInt(), equals(-1)); + expect(reader.readVarInt(), equals(-64)); + expect(reader.readVarInt(), equals(-1000)); + }); + + test('write and read VarInt mixed positive and negative', () { + final writer = BinaryWriter() + ..writeVarInt(-1) + ..writeVarInt(1) + ..writeVarInt(-100) + ..writeVarInt(100) + ..writeVarInt(-10000) + ..writeVarInt(10000); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarInt(), equals(-1)); + expect(reader.readVarInt(), equals(1)); + expect(reader.readVarInt(), equals(-100)); + expect(reader.readVarInt(), equals(100)); + expect(reader.readVarInt(), equals(-10000)); + expect(reader.readVarInt(), equals(10000)); + }); + }); + + group('Boolean operations', () { + test('write and read single boolean true', () { + final writer = BinaryWriter()..writeBool(true); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readBool(), isTrue); + }); + + test('write and read single boolean false', () { + final writer = BinaryWriter()..writeBool(false); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readBool(), isFalse); + }); + + test('write and read multiple booleans', () { + final writer = BinaryWriter() + ..writeBool(true) + ..writeBool(false) + ..writeBool(true) + ..writeBool(true) + ..writeBool(false); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isTrue); + expect(reader.readBool(), isFalse); + }); + + test('write and read booleans mixed with other types', () { + final writer = BinaryWriter() + ..writeBool(true) + ..writeUint32(42) + ..writeBool(false) + ..writeString('test') + ..writeBool(true); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readBool(), isTrue); + expect(reader.readUint32(), equals(42)); + expect(reader.readBool(), isFalse); + expect(reader.readString(4), equals('test')); + expect(reader.readBool(), isTrue); + }); + }); + + group('VarBytes operations', () { + test('write and read VarBytes empty array', () { + final writer = BinaryWriter()..writeVarBytes([]); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readVarBytes(); + + expect(result, isEmpty); + }); + + test('write and read VarBytes small array', () { + final writer = BinaryWriter()..writeVarBytes([1, 2, 3, 4, 5]); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readVarBytes(); + + expect(result, equals([1, 2, 3, 4, 5])); + }); + + test('write and read VarBytes large array', () { + final data = List.generate(1000, (i) => i % 256); + final writer = BinaryWriter()..writeVarBytes(data); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + final result = reader.readVarBytes(); + + expect(result, equals(data)); + }); + + test('write and read multiple VarBytes', () { + final writer = BinaryWriter() + ..writeVarBytes([1, 2, 3]) + ..writeVarBytes([4, 5]) + ..writeVarBytes([6, 7, 8, 9]); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarBytes(), equals([1, 2, 3])); + expect(reader.readVarBytes(), equals([4, 5])); + expect(reader.readVarBytes(), equals([6, 7, 8, 9])); + }); + }); + + group('VarString operations', () { + test('write and read VarString empty', () { + final writer = BinaryWriter()..writeVarString(''); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('')); + }); + + test('write and read VarString ASCII', () { + final writer = BinaryWriter()..writeVarString('Hello, World!'); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('Hello, World!')); + }); + + test('write and read VarString UTF-8', () { + final writer = BinaryWriter()..writeVarString('Привет, мир! 你好世界 🚀'); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('Привет, мир! 你好世界 🚀')); + }); + + test('write and read multiple VarStrings', () { + final writer = BinaryWriter() + ..writeVarString('First') + ..writeVarString('Second') + ..writeVarString('Third'); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarString(), equals('First')); + expect(reader.readVarString(), equals('Second')); + expect(reader.readVarString(), equals('Third')); + }); + + test('write and read VarString mixed with other types', () { + final writer = BinaryWriter() + ..writeVarString('Start') + ..writeUint32(42) + ..writeVarString('Middle') + ..writeVarUint(100) + ..writeVarString('End'); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarString(), equals('Start')); + expect(reader.readUint32(), equals(42)); + expect(reader.readVarString(), equals('Middle')); + expect(reader.readVarUint(), equals(100)); + expect(reader.readVarString(), equals('End')); + }); + }); + + group('Reader navigation operations', () { + test('seek to specific position and read', () { + final writer = BinaryWriter() + ..writeUint32(100) + ..writeUint32(200) + ..writeUint32(300); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes)..seek(4); + expect(reader.readUint32(), equals(200)); + + reader.seek(0); + expect(reader.readUint32(), equals(100)); + + reader.seek(8); + expect(reader.readUint32(), equals(300)); + }); + + test('rewind and re-read data', () { + final writer = BinaryWriter() + ..writeUint32(42) + ..writeUint32(84); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + final first = reader.readUint32(); + expect(first, equals(42)); + + reader.rewind(4); + expect(reader.readUint32(), equals(42)); + }); + + test('hasBytes checks availability correctly', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeUint16(2) + ..writeUint32(3); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.hasBytes(7), isTrue); + expect(reader.hasBytes(8), isFalse); + + reader.readUint8(); + expect(reader.hasBytes(6), isTrue); + expect(reader.hasBytes(7), isFalse); + }); + + test('readRemainingBytes reads all remaining data', () { + final writer = BinaryWriter() + ..writeUint32(42) + ..writeBytes([1, 2, 3, 4, 5]); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes)..readUint32(); + final remaining = reader.readRemainingBytes(); + + expect(remaining, equals([1, 2, 3, 4, 5])); + expect(reader.availableBytes, equals(0)); + }); + + test('combined navigation operations', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeUint8(2) + ..writeUint8(3) + ..writeUint8(4) + ..writeUint8(5); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readUint8(), equals(1)); + reader.skip(2); + expect(reader.readUint8(), equals(4)); + reader.rewind(2); + expect(reader.readUint8(), equals(3)); + reader.seek(0); + expect(reader.readUint8(), equals(1)); + }); + }); + + group('Advanced writer features', () { + test('bytesWritten tracks written data correctly', () { + final writer = BinaryWriter(); + expect(writer.bytesWritten, equals(0)); + + writer.writeUint8(42); + expect(writer.bytesWritten, equals(1)); + + writer.writeUint32(100); + expect(writer.bytesWritten, equals(5)); + + writer.writeString('Hello'); + expect(writer.bytesWritten, equals(10)); + }); + + test('writeBytes with offset and length', () { + final data = Uint8List.fromList([10, 20, 30, 40, 50, 60]); + final writer = BinaryWriter()..writeBytes(data, 2, 3); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readBytes(3), equals([30, 40, 50])); + }); + + test('writeBytes with offset only', () { + final data = Uint8List.fromList([10, 20, 30, 40, 50]); + final writer = BinaryWriter()..writeBytes(data, 2); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readBytes(3), equals([30, 40, 50])); + }); + }); + + group('Real-world message format simulation', () { + test('length-prefixed message with VarInt', () { + final writer = BinaryWriter(); + const message = 'This is a test message'; + final messageBytes = utf8.encode(message); + + writer + ..writeVarUint(messageBytes.length) + ..writeBytes(messageBytes); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + final length = reader.readVarUint(); + final receivedMessage = reader.readString(length); + + expect(receivedMessage, equals(message)); + }); + + test('protocol with header and payload', () { + final writer = BinaryWriter() + // Header + ..writeUint8(1) // version + ..writeUint8(42) // message type + ..writeUint32(123456) // message id + // Payload + ..writeVarString('user@example.com') + ..writeVarUint(1000) + ..writeBool(true); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + // Read header + expect(reader.readUint8(), equals(1)); + expect(reader.readUint8(), equals(42)); + expect(reader.readUint32(), equals(123456)); + + // Read payload + expect(reader.readVarString(), equals('user@example.com')); + expect(reader.readVarUint(), equals(1000)); + expect(reader.readBool(), isTrue); + }); + + test('array of structures with VarInt lengths', () { + final writer = BinaryWriter(); + final items = [ + {'name': 'Item1', 'value': 100}, + {'name': 'Item2', 'value': 200}, + {'name': 'Item3', 'value': 300}, + ]; + + writer.writeVarUint(items.length); + for (final item in items) { + writer + ..writeVarString(item['name']! as String) + ..writeVarUint(item['value']! as int); + } + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + final count = reader.readVarUint(); + expect(count, equals(3)); + + for (var i = 0; i < count; i++) { + expect(reader.readVarString(), equals(items[i]['name'])); + expect(reader.readVarUint(), equals(items[i]['value'])); + } + }); + + test('conditional reading with hasBytes', () { + final writer = BinaryWriter() + ..writeUint32(42) + ..writeUint16(100); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readUint32(), equals(42)); + + // Check if there's data for another uint32, but only uint16 available + if (reader.hasBytes(4)) { + fail('Should not have 4 bytes'); + } else if (reader.hasBytes(2)) { + expect(reader.readUint16(), equals(100)); + } + }); + }); }); } diff --git a/test/performance/writer/buffer_growth_bench_test.dart b/test/performance/writer/buffer_growth_bench_test.dart index f71b7e2..dfd6b97 100644 --- a/test/performance/writer/buffer_growth_bench_test.dart +++ b/test/performance/writer/buffer_growth_bench_test.dart @@ -154,8 +154,8 @@ class BufferGrowthMixedWritesBenchmark extends BenchmarkBase { for (var i = 0; i < 200; i++) { writer ..writeUint8(i % 256) - ..writeUint32(i * 1000, Endian.little) - ..writeFloat64(i * 3.14, Endian.little) + ..writeUint32(i * 1000, .little) + ..writeFloat64(i * 3.14, .little) ..writeString('Message $i') ..writeVarUint(i); } diff --git a/test/performance/writer/fixed_int_write_bench_test.dart b/test/performance/writer/fixed_int_write_bench_test.dart index e23f578..f14437b 100644 --- a/test/performance/writer/fixed_int_write_bench_test.dart +++ b/test/performance/writer/fixed_int_write_bench_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; @@ -78,7 +76,7 @@ class Uint16LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeUint16(i % 65536, Endian.little); + writer.writeUint16(i % 65536, .little); } writer.reset(); } @@ -118,7 +116,7 @@ class Int16LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeInt16((i % 65536) - 32768, Endian.little); + writer.writeInt16((i % 65536) - 32768, .little); } writer.reset(); } @@ -158,7 +156,7 @@ class Uint32LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeUint32(i * 1000, Endian.little); + writer.writeUint32(i * 1000, .little); } writer.reset(); } @@ -198,7 +196,7 @@ class Int32LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeInt32(i * 1000 - 500000, Endian.little); + writer.writeInt32(i * 1000 - 500000, .little); } writer.reset(); } @@ -238,7 +236,7 @@ class Uint64LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeUint64(i * 1000000, Endian.little); + writer.writeUint64(i * 1000000, .little); } writer.reset(); } @@ -278,7 +276,7 @@ class Int64LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeInt64(i * 1000000 - 500000000, Endian.little); + writer.writeInt64(i * 1000000 - 500000000, .little); } writer.reset(); } @@ -300,13 +298,13 @@ class MixedFixedIntWriteBenchmark extends BenchmarkBase { for (var i = 0; i < 100; i++) { writer ..writeUint8(i % 256) - ..writeUint16(i % 65536, Endian.little) - ..writeUint32(i * 1000, Endian.little) - ..writeInt32(i * 100 - 5000, Endian.little) - ..writeUint64(i * 1000000, Endian.little) + ..writeUint16(i % 65536, .little) + ..writeUint32(i * 1000, .little) + ..writeInt32(i * 100 - 5000, .little) + ..writeUint64(i * 1000000, .little) ..writeInt8((i % 256) - 128) - ..writeInt16((i % 32768) - 16384, Endian.little) - ..writeInt64(i * 1000000, Endian.little); + ..writeInt16((i % 32768) - 16384, .little) + ..writeInt64(i * 1000000, .little); } writer.reset(); } diff --git a/test/performance/writer/float_write_bench_test.dart b/test/performance/writer/float_write_bench_test.dart index 385a82f..6693604 100644 --- a/test/performance/writer/float_write_bench_test.dart +++ b/test/performance/writer/float_write_bench_test.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:benchmark_harness/benchmark_harness.dart'; import 'package:pro_binary/pro_binary.dart'; import 'package:test/test.dart'; @@ -38,7 +36,7 @@ class Float32LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeFloat32((i * 3.14159) - 500.0, Endian.little); + writer.writeFloat32((i * 3.14159) - 500.0, .little); } writer.reset(); } @@ -58,7 +56,7 @@ class Float32SmallValuesWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeFloat32((i % 100) * 0.01, Endian.little); + writer.writeFloat32((i % 100) * 0.01, .little); } writer.reset(); } @@ -78,7 +76,7 @@ class Float32LargeValuesWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeFloat32((i * 1000000.0) - 500000000.0, Endian.little); + writer.writeFloat32((i * 1000000.0) - 500000000.0, .little); } writer.reset(); } @@ -100,10 +98,10 @@ class Float32SpecialValuesWriteBenchmark extends BenchmarkBase { void run() { for (var i = 0; i < 250; i++) { writer - ..writeFloat32(0, Endian.little) - ..writeFloat32(double.nan, Endian.little) - ..writeFloat32(double.infinity, Endian.little) - ..writeFloat32(double.negativeInfinity, Endian.little); + ..writeFloat32(0, .little) + ..writeFloat32(.nan, .little) + ..writeFloat32(.infinity, .little) + ..writeFloat32(.negativeInfinity, .little); } writer.reset(); } @@ -143,7 +141,7 @@ class Float64LittleEndianWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeFloat64((i * 2.718281828) - 1000.0, Endian.little); + writer.writeFloat64((i * 2.718281828) - 1000.0, .little); } writer.reset(); } @@ -163,7 +161,7 @@ class Float64SmallValuesWriteBenchmark extends BenchmarkBase { @override void run() { for (var i = 0; i < 1000; i++) { - writer.writeFloat64((i % 100) * 0.001, Endian.little); + writer.writeFloat64((i % 100) * 0.001, .little); } writer.reset(); } @@ -185,7 +183,7 @@ class Float64LargeValuesWriteBenchmark extends BenchmarkBase { for (var i = 0; i < 1000; i++) { writer.writeFloat64( (i * 1000000000.0) - 500000000000.0, - Endian.little, + .little, ); } writer.reset(); @@ -208,10 +206,10 @@ class Float64SpecialValuesWriteBenchmark extends BenchmarkBase { void run() { for (var i = 0; i < 250; i++) { writer - ..writeFloat64(0, Endian.little) - ..writeFloat64(double.nan, Endian.little) - ..writeFloat64(double.infinity, Endian.little) - ..writeFloat64(double.negativeInfinity, Endian.little); + ..writeFloat64(0, .little) + ..writeFloat64(.nan, .little) + ..writeFloat64(.infinity, .little) + ..writeFloat64(.negativeInfinity, .little); } writer.reset(); } @@ -233,21 +231,21 @@ class MixedFloatWriteBenchmark extends BenchmarkBase { for (var i = 0; i < 100; i++) { writer // Position (3D coordinates) - ..writeFloat32(i * 10.0, Endian.little) - ..writeFloat32(i * 20.0, Endian.little) - ..writeFloat32(i * 30.0, Endian.little) + ..writeFloat32(i * 10.0, .little) + ..writeFloat32(i * 20.0, .little) + ..writeFloat32(i * 30.0, .little) // Rotation (quaternion) - ..writeFloat32(0, Endian.little) - ..writeFloat32(0, Endian.little) - ..writeFloat32(0, Endian.little) - ..writeFloat32(1, Endian.little) + ..writeFloat32(0, .little) + ..writeFloat32(0, .little) + ..writeFloat32(0, .little) + ..writeFloat32(1, .little) // Timestamp - ..writeFloat64(i * 0.016, Endian.little) + ..writeFloat64(i * 0.016, .little) // Color (RGBA) - ..writeFloat32(1, Endian.little) - ..writeFloat32(0.5, Endian.little) - ..writeFloat32(0, Endian.little) - ..writeFloat32(1, Endian.little); + ..writeFloat32(1, .little) + ..writeFloat32(0.5, .little) + ..writeFloat32(0, .little) + ..writeFloat32(1, .little); } writer.reset(); } @@ -268,8 +266,8 @@ class AlternatingFloatWriteBenchmark extends BenchmarkBase { void run() { for (var i = 0; i < 500; i++) { writer - ..writeFloat32(i * 3.14, Endian.little) - ..writeFloat64(i * 2.718, Endian.little); + ..writeFloat32(i * 3.14, .little) + ..writeFloat64(i * 2.718, .little); } writer.reset(); } diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart index 22c0520..139c5cd 100644 --- a/test/unit/binary_reader_test.dart +++ b/test/unit/binary_reader_test.dart @@ -787,7 +787,7 @@ void main() { test('readFloat32 with NaN', () { final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, double.nan); + ByteData.view(buffer.buffer).setFloat32(0, .nan); final reader = BinaryReader(buffer); expect(reader.readFloat32().isNaN, isTrue); @@ -795,7 +795,7 @@ void main() { test('readFloat32 with Infinity', () { final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, double.infinity); + ByteData.view(buffer.buffer).setFloat32(0, .infinity); final reader = BinaryReader(buffer); expect(reader.readFloat32(), equals(double.infinity)); @@ -803,7 +803,7 @@ void main() { test('readFloat32 with negative Infinity', () { final buffer = Uint8List(4); - ByteData.view(buffer.buffer).setFloat32(0, double.negativeInfinity); + ByteData.view(buffer.buffer).setFloat32(0, .negativeInfinity); final reader = BinaryReader(buffer); expect(reader.readFloat32(), equals(double.negativeInfinity)); @@ -811,7 +811,7 @@ void main() { test('readFloat64 with NaN', () { final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, double.nan); + ByteData.view(buffer.buffer).setFloat64(0, .nan); final reader = BinaryReader(buffer); expect(reader.readFloat64().isNaN, isTrue); @@ -819,7 +819,7 @@ void main() { test('readFloat64 with Infinity', () { final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, double.infinity); + ByteData.view(buffer.buffer).setFloat64(0, .infinity); final reader = BinaryReader(buffer); expect(reader.readFloat64(), equals(double.infinity)); @@ -827,7 +827,7 @@ void main() { test('readFloat64 with negative Infinity', () { final buffer = Uint8List(8); - ByteData.view(buffer.buffer).setFloat64(0, double.negativeInfinity); + ByteData.view(buffer.buffer).setFloat64(0, .negativeInfinity); final reader = BinaryReader(buffer); expect(reader.readFloat64(), equals(double.negativeInfinity)); diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index 69d2437..cef6768 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -618,7 +618,7 @@ void main() { }); test('handle Float32 NaN value correctly', () { - writer.writeFloat32(double.nan); + writer.writeFloat32(.nan); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -626,7 +626,7 @@ void main() { }); test('handle Float32 positive Infinity correctly', () { - writer.writeFloat32(double.infinity); + writer.writeFloat32(.infinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -634,7 +634,7 @@ void main() { }); test('handle Float32 negative Infinity correctly', () { - writer.writeFloat32(double.negativeInfinity); + writer.writeFloat32(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -642,7 +642,7 @@ void main() { }); test('handle Float64 NaN value correctly', () { - writer.writeFloat64(double.nan); + writer.writeFloat64(.nan); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -650,7 +650,7 @@ void main() { }); test('handle Float64 positive Infinity correctly', () { - writer.writeFloat64(double.infinity); + writer.writeFloat64(.infinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); @@ -658,7 +658,7 @@ void main() { }); test('handle Float64 negative Infinity correctly', () { - writer.writeFloat64(double.negativeInfinity); + writer.writeFloat64(.negativeInfinity); final bytes = writer.takeBytes(); final reader = BinaryReader(bytes); From eae598906b3a4ac12e8d3b2856c1a9a6f7c6aef4 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Tue, 6 Jan 2026 12:15:21 +0200 Subject: [PATCH 24/29] Add edge case tests for VarInt and VarUint in BinaryWriter and BinaryReader --- test/unit/binary_reader_test.dart | 205 ++++++++++++++++++++++++++ test/unit/binary_writer_test.dart | 232 ++++++++++++++++++++++++++++++ 2 files changed, 437 insertions(+) diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart index 139c5cd..914c605 100644 --- a/test/unit/binary_reader_test.dart +++ b/test/unit/binary_reader_test.dart @@ -1866,5 +1866,210 @@ void main() { expect(() => reader.rewind(-1), throwsA(isA())); }); }); + + group('VarInt/VarUint edge cases', () { + test('readVarUint with maximum safe 64-bit value boundary', () { + // Test value close to overflow boundary + final writer = BinaryWriter()..writeVarUint(0x7FFFFFFFFFFFFFFF); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(0x7FFFFFFFFFFFFFFF)); + }); + + test('readVarInt with maximum positive ZigZag value', () { + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + final writer = BinaryWriter()..writeVarInt(0x3FFFFFFFFFFFFFFF); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + expect(reader.readVarInt(), equals(0x3FFFFFFFFFFFFFFF)); + }); + + test('readVarInt with minimum negative ZigZag value', () { + final writer = BinaryWriter()..writeVarInt(-0x4000000000000000); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarInt(), equals(-0x4000000000000000)); + }); + + test('readVarUint boundary values sequence', () { + final writer = BinaryWriter() + ..writeVarUint(0x7F) // 1 byte max + ..writeVarUint(0x80) // 2 byte min + ..writeVarUint(0x3FFF) // 2 byte max + ..writeVarUint(0x4000) // 3 byte min + ..writeVarUint(0x1FFFFF) // 3 byte max + ..writeVarUint(0x200000); // 4 byte min + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(0x7F)); + expect(reader.readVarUint(), equals(0x80)); + expect(reader.readVarUint(), equals(0x3FFF)); + expect(reader.readVarUint(), equals(0x4000)); + expect(reader.readVarUint(), equals(0x1FFFFF)); + expect(reader.readVarUint(), equals(0x200000)); + }); + }); + + group('VarBytes/VarString error handling', () { + test('readVarBytes throws when length exceeds available bytes', () { + // Write VarInt claiming 1000 bytes but only provide 10 + final writer = BinaryWriter() + ..writeVarUint(1000) + ..writeBytes(List.filled(10, 42)); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarBytes, throwsA(isA())); + }); + + test('readVarString throws when length exceeds available bytes', () { + // Write VarInt claiming 100 bytes but only provide 5 + final writer = BinaryWriter() + ..writeVarUint(100) + ..writeBytes([72, 101, 108, 108, 111]); // "Hello" + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarString, throwsA(isA())); + }); + + test('readVarBytes with corrupted length at buffer end', () { + // VarInt that claims more bytes than buffer has + final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); + final reader = BinaryReader(buffer); + + // Should throw when trying to read the claimed bytes + expect(reader.readVarBytes, throwsA(isA())); + }); + + test('readVarString handles empty string correctly', () { + final writer = BinaryWriter()..writeVarString(''); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals('')); + }); + + test('readVarBytes with zero length', () { + final writer = BinaryWriter()..writeVarBytes([]); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarBytes(), isEmpty); + }); + + test('readVarString with malformed UTF-8 in VarString format', () { + // Write invalid UTF-8 sequence with VarInt length prefix + final writer = BinaryWriter() + ..writeVarUint(3) + ..writeBytes([0xFF, 0xFE, 0xFD]); // Invalid UTF-8 + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect( + reader.readVarString, + throwsA(isA()), + ); + + // Reset and try with allowMalformed + final reader2 = BinaryReader(bytes); + final result = reader2.readVarString(allowMalformed: true); + expect(result, isNotEmpty); // Should contain replacement characters + }); + }); + + group('Partial read scenarios', () { + test('reading after partial VarInt consumption', () { + final writer = BinaryWriter() + ..writeVarUint(300) + ..writeUint32(0x12345678); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(300)); + expect(reader.readUint32(), equals(0x12345678)); + expect(reader.availableBytes, equals(0)); + }); + + test('interleaved VarInt and fixed-size reads', () { + final writer = BinaryWriter() + ..writeVarUint(127) + ..writeUint8(42) + ..writeVarInt(-1) + ..writeUint16(1000) + ..writeVarUint(128) + ..writeUint32(0xDEADBEEF); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(127)); + expect(reader.readUint8(), equals(42)); + expect(reader.readVarInt(), equals(-1)); + expect(reader.readUint16(), equals(1000)); + expect(reader.readVarUint(), equals(128)); + expect(reader.readUint32(), equals(0xDEADBEEF)); + }); + + test('readRemainingBytes after VarBytes', () { + final writer = BinaryWriter() + ..writeVarBytes([1, 2, 3]) + ..writeBytes([4, 5, 6, 7, 8]); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + final varBytes = reader.readVarBytes(); + expect(varBytes, equals([1, 2, 3])); + + final remaining = reader.readRemainingBytes(); + expect(remaining, equals([4, 5, 6, 7, 8])); + }); + }); + + group('Navigation edge cases', () { + test('seek and hasBytes combined', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer)..seek(3); + expect(reader.hasBytes(5), isTrue); + expect(reader.hasBytes(6), isFalse); + + reader.seek(7); + expect(reader.hasBytes(1), isTrue); + expect(reader.hasBytes(2), isFalse); + }); + + test('rewind to exactly zero offset', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5]); + final reader = BinaryReader(buffer)..readBytes(3); + expect(reader.offset, equals(3)); + + reader.rewind(3); + expect(reader.offset, equals(0)); + expect(reader.readUint8(), equals(1)); + }); + + test('multiple seeks without reading', () { + final buffer = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]); + final reader = BinaryReader(buffer); + + for (var i = 0; i < 8; i++) { + reader.seek(i); + expect(reader.offset, equals(i)); + } + }); + }); }); } diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index cef6768..d765d18 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -2184,4 +2184,236 @@ void main() { BinaryWriterPool.release(writer2); }); }); + + group('VarInt/VarUint edge cases', () { + test('writeVarUint with maximum safe 64-bit value', () { + final writer = BinaryWriter()..writeVarUint(0x7FFFFFFFFFFFFFFF); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(0x7FFFFFFFFFFFFFFF)); + }); + + test('writeVarInt with maximum positive value', () { + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + final writer = BinaryWriter()..writeVarInt(0x3FFFFFFFFFFFFFFF); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + // disabling lint for large integer literal + // ignore: avoid_js_rounded_ints + expect(reader.readVarInt(), equals(0x3FFFFFFFFFFFFFFF)); + }); + + test('writeVarInt with minimum negative value', () { + final writer = BinaryWriter()..writeVarInt(-0x4000000000000000); + final bytes = writer.takeBytes(); + + final reader = BinaryReader(bytes); + expect(reader.readVarInt(), equals(-0x4000000000000000)); + }); + + test('writeVarUint boundary transitions', () { + final writer = BinaryWriter() + ..writeVarUint(0x7F) // Last 1-byte value + ..writeVarUint(0x80) // First 2-byte value + ..writeVarUint(0x3FFF) // Last 2-byte value + ..writeVarUint(0x4000) // First 3-byte value + ..writeVarUint(0x1FFFFF) // Last 3-byte value + ..writeVarUint(0x200000); // First 4-byte value + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarUint(), equals(0x7F)); + expect(reader.readVarUint(), equals(0x80)); + expect(reader.readVarUint(), equals(0x3FFF)); + expect(reader.readVarUint(), equals(0x4000)); + expect(reader.readVarUint(), equals(0x1FFFFF)); + expect(reader.readVarUint(), equals(0x200000)); + }); + + test('writeVarInt ZigZag boundary transitions', () { + final writer = BinaryWriter() + ..writeVarInt(-64) // Last 1-byte negative + ..writeVarInt(-65) // First 2-byte negative + ..writeVarInt(63) // Last 1-byte positive + ..writeVarInt(64); // First 2-byte positive + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarInt(), equals(-64)); + expect(reader.readVarInt(), equals(-65)); + expect(reader.readVarInt(), equals(63)); + expect(reader.readVarInt(), equals(64)); + }); + + test('writeVarUint with all 10-byte value (near maximum)', () { + // Maximum VarUint uses 9-10 bytes depending on value + const largeValue = 0x7FFFFFFFFFFFFFFF; + final writer = BinaryWriter()..writeVarUint(largeValue); + final bytes = writer.takeBytes(); + + expect(bytes.length, equals(9)); // This value encodes to 9 bytes + + final reader = BinaryReader(bytes); + expect(reader.readVarUint(), equals(largeValue)); + }); + }); + + group('VarBytes/VarString edge cases', () { + test('writeVarBytes with maximum single-byte length (127 bytes)', () { + final writer = BinaryWriter(); + final data = List.generate(127, (i) => i); + writer.writeVarBytes(data); + final bytes = writer.takeBytes(); + + // VarUint(127) = 1 byte + 127 data bytes = 128 total + expect(bytes.length, equals(128)); + expect(bytes[0], equals(127)); + + final reader = BinaryReader(bytes); + expect(reader.readVarBytes(), equals(data)); + }); + + test('writeVarBytes with minimum two-byte length (128 bytes)', () { + final writer = BinaryWriter(); + final data = List.generate(128, (i) => i & 0xFF); + writer.writeVarBytes(data); + final bytes = writer.takeBytes(); + + // VarUint(128) = 2 bytes + 128 data bytes = 130 total + expect(bytes.length, equals(130)); + + final reader = BinaryReader(bytes); + expect(reader.readVarBytes(), equals(data)); + }); + + test('writeVarString with ASCII at 127 character boundary', () { + final writer = BinaryWriter(); + final str = 'A' * 127; // 127 ASCII chars = 127 bytes + writer.writeVarString(str); + final bytes = writer.takeBytes(); + + // VarUint(127) = 1 byte + 127 bytes = 128 total + expect(bytes.length, equals(128)); + + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals(str)); + }); + + test('writeVarString with UTF-8 multi-byte at boundary', () { + final writer = BinaryWriter(); + // Each Cyrillic char = 2 bytes, 64 chars = 128 bytes + final str = 'Я' * 64; + writer.writeVarString(str); + final bytes = writer.takeBytes(); + + // VarUint(128) = 2 bytes + 128 bytes = 130 total + expect(bytes.length, equals(130)); + + final reader = BinaryReader(bytes); + expect(reader.readVarString(), equals(str)); + }); + + test('writeVarBytes triggers buffer expansion', () { + final writer = BinaryWriter(initialBufferSize: 16); + final largeData = List.generate(1000, (i) => i & 0xFF); + + writer.writeVarBytes(largeData); + + final bytes = writer.takeBytes(); + expect(bytes.length, greaterThan(1000)); + + final reader = BinaryReader(bytes); + expect(reader.readVarBytes(), equals(largeData)); + }); + }); + + group('Complex error scenarios', () { + test( + 'writeString with extremely long string triggers multiple expansions', + () { + final writer = BinaryWriter(initialBufferSize: 8); + final longString = 'A' * 10000; + + writer.writeString(longString); + final bytes = writer.takeBytes(); + + expect(bytes.length, equals(10000)); + + final reader = BinaryReader(bytes); + expect(reader.readString(bytes.length), equals(longString)); + }, + ); + + test('alternating VarInt and fixed writes with buffer growth', () { + final writer = BinaryWriter(initialBufferSize: 16); + + for (var i = 0; i < 50; i++) { + writer + ..writeVarUint(i * 100) + ..writeUint32(i) + ..writeVarInt(-i); + } + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + for (var i = 0; i < 50; i++) { + expect(reader.readVarUint(), equals(i * 100)); + expect(reader.readUint32(), equals(i)); + expect(reader.readVarInt(), equals(-i)); + } + }); + + test('writeVarString with mixed malformed and valid UTF-8', () { + final writer = BinaryWriter() + // Valid string first + ..writeVarString('Valid'); + + // Malformed string with allowMalformed=true + const malformed = 'Test\uD800End'; + writer.writeVarString(malformed); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readVarString(), equals('Valid')); + final result = reader.readVarString(allowMalformed: true); + expect(result, contains('Test')); + expect(result, contains('End')); + }); + + test('complex interleaved writes maintain correct offsets', () { + final writer = BinaryWriter() + ..writeUint8(1) + ..writeVarUint(300) + ..writeUint16(1000) + ..writeVarInt(-500) + ..writeUint32(0xDEADBEEF) + ..writeVarString('Test') + ..writeBool(true) + ..writeVarBytes([1, 2, 3, 4, 5]) + ..writeFloat32(3.14) + ..writeUint64(123456789); + + final bytes = writer.takeBytes(); + final reader = BinaryReader(bytes); + + expect(reader.readUint8(), equals(1)); + expect(reader.readVarUint(), equals(300)); + expect(reader.readUint16(), equals(1000)); + expect(reader.readVarInt(), equals(-500)); + expect(reader.readUint32(), equals(0xDEADBEEF)); + expect(reader.readVarString(), equals('Test')); + expect(reader.readBool(), isTrue); + expect(reader.readVarBytes(), equals([1, 2, 3, 4, 5])); + expect(reader.readFloat32(), closeTo(3.14, 0.01)); + expect(reader.readUint64(), equals(123456789)); + expect(reader.availableBytes, equals(0)); + }); + }); } From 0f5175f7c5f822c0ce5f864b5261061d00a2c374 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Wed, 7 Jan 2026 12:12:38 +0200 Subject: [PATCH 25/29] Refactor tests and benchmarks for clarity and consistency - Updated test descriptions in `binary_reader_test.dart` for better readability. - Enhanced test descriptions in `binary_writer_test.dart` to reflect expected behavior. - Added `exercise` method to various benchmarks to standardize execution. - Renamed integration test for buffer management to emphasize data integrity. - Removed redundant round-trip tests in `binary_reader_test.dart` and `binary_writer_test.dart`. --- test/integration/integration_test.dart | 2 +- .../writer/buffer_growth_bench_test.dart | 36 +++++ .../writer/fixed_int_write_bench_test.dart | 45 +++++++ .../writer/float_write_bench_test.dart | 36 +++++ test/performance/writer/pool_bench_test.dart | 30 +++++ .../writer/string_write_bench_test.dart | 39 ++++++ .../writer/varint_write_bench_test.dart | 27 ++++ test/unit/binary_reader_test.dart | 123 +++++++----------- test/unit/binary_writer_test.dart | 92 +++++-------- 9 files changed, 293 insertions(+), 137 deletions(-) diff --git a/test/integration/integration_test.dart b/test/integration/integration_test.dart index 5622db8..7a6b8d8 100644 --- a/test/integration/integration_test.dart +++ b/test/integration/integration_test.dart @@ -665,7 +665,7 @@ void main() { }); group('Writer buffer management integration', () { - test('write causes buffer expansion and can be read correctly', () { + test('buffer expansion preserves data integrity', () { final writer = BinaryWriter(initialBufferSize: 4); for (var i = 0; i < 100; i++) { diff --git a/test/performance/writer/buffer_growth_bench_test.dart b/test/performance/writer/buffer_growth_bench_test.dart index dfd6b97..9b791b7 100644 --- a/test/performance/writer/buffer_growth_bench_test.dart +++ b/test/performance/writer/buffer_growth_bench_test.dart @@ -16,6 +16,9 @@ class BufferGrowthSmallInitialBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16); } + @override + void exercise() => run(); + @override void run() { // Write 1KB of data, forcing multiple expansions @@ -38,6 +41,9 @@ class BufferGrowthMediumInitialBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 256); } + @override + void exercise() => run(); + @override void run() { // Write 64KB of data @@ -61,6 +67,9 @@ class BufferGrowthIncrementalBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 64); } + @override + void exercise() => run(); + @override void run() { // Write progressively larger chunks @@ -88,6 +97,9 @@ class BufferGrowthLargeSingleWriteBenchmark extends BenchmarkBase { largeData = Uint8List.fromList(List.generate(32768, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { // Single large write that forces expansion @@ -108,6 +120,9 @@ class BufferGrowthStringWritesBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32); } + @override + void exercise() => run(); + @override void run() { const testString = 'Hello World! This is a test string.'; @@ -129,6 +144,9 @@ class BufferGrowthVarIntWritesBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 1024); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 250; i++) { @@ -149,6 +167,9 @@ class BufferGrowthMixedWritesBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 64); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 200; i++) { @@ -175,6 +196,9 @@ class NoBufferGrowthBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { // Write 32KB without triggering growth @@ -199,6 +223,9 @@ class BufferGrowthVarBytesBenchmark extends BenchmarkBase { data = Uint8List.fromList(List.generate(32, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -220,6 +247,9 @@ class BufferGrowthResetPatternBenchmark extends BenchmarkBase { writer = BinaryWriter(); } + @override + void exercise() => run(); + @override void run() { // First write: small @@ -258,6 +288,9 @@ class BufferGrowthAlternatingSizesBenchmark extends BenchmarkBase { largeData = Uint8List.fromList(List.generate(512, (i) => i % 256)); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 50; i++) { @@ -282,6 +315,9 @@ class BufferGrowthMaxCapacityBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 1024); } + @override + void exercise() => run(); + @override void run() { // Write exactly 64KB to test max reusable capacity diff --git a/test/performance/writer/fixed_int_write_bench_test.dart b/test/performance/writer/fixed_int_write_bench_test.dart index f14437b..c43156d 100644 --- a/test/performance/writer/fixed_int_write_bench_test.dart +++ b/test/performance/writer/fixed_int_write_bench_test.dart @@ -13,6 +13,9 @@ class Uint8WriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -33,6 +36,9 @@ class Int8WriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -53,6 +59,9 @@ class Uint16BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -73,6 +82,9 @@ class Uint16LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -93,6 +105,9 @@ class Int16BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -113,6 +128,9 @@ class Int16LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -133,6 +151,9 @@ class Uint32BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -153,6 +174,9 @@ class Uint32LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -173,6 +197,9 @@ class Int32BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -193,6 +220,9 @@ class Int32LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -213,6 +243,9 @@ class Uint64BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -233,6 +266,9 @@ class Uint64LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -253,6 +289,9 @@ class Int64BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -273,6 +312,9 @@ class Int64LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -293,6 +335,9 @@ class MixedFixedIntWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { diff --git a/test/performance/writer/float_write_bench_test.dart b/test/performance/writer/float_write_bench_test.dart index 6693604..4ed162e 100644 --- a/test/performance/writer/float_write_bench_test.dart +++ b/test/performance/writer/float_write_bench_test.dart @@ -13,6 +13,9 @@ class Float32BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -33,6 +36,9 @@ class Float32LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -53,6 +59,9 @@ class Float32SmallValuesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -73,6 +82,9 @@ class Float32LargeValuesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -94,6 +106,9 @@ class Float32SpecialValuesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 250; i++) { @@ -118,6 +133,9 @@ class Float64BigEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -138,6 +156,9 @@ class Float64LittleEndianWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -158,6 +179,9 @@ class Float64SmallValuesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -178,6 +202,9 @@ class Float64LargeValuesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -202,6 +229,9 @@ class Float64SpecialValuesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 250; i++) { @@ -226,6 +256,9 @@ class MixedFloatWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -262,6 +295,9 @@ class AlternatingFloatWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 500; i++) { diff --git a/test/performance/writer/pool_bench_test.dart b/test/performance/writer/pool_bench_test.dart index f5f92ef..e56bd48 100644 --- a/test/performance/writer/pool_bench_test.dart +++ b/test/performance/writer/pool_bench_test.dart @@ -13,6 +13,9 @@ class PoolAcquireNewBenchmark extends BenchmarkBase { BinaryWriterPool.clear(); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -43,6 +46,9 @@ class PoolAcquireReusedBenchmark extends BenchmarkBase { writers.forEach(BinaryWriterPool.release); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -69,6 +75,9 @@ class PoolReleaseBenchmark extends BenchmarkBase { } } + @override + void exercise() => run(); + @override void run() { for (final writer in writers) { @@ -90,6 +99,9 @@ class PoolFullCycleBenchmark extends BenchmarkBase { BinaryWriterPool.clear(); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -113,6 +125,9 @@ class PoolHeavyUsageBenchmark extends BenchmarkBase { BinaryWriterPool.clear(); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 50; i++) { @@ -143,6 +158,9 @@ class PoolSequentialAcquireBenchmark extends BenchmarkBase { BinaryWriterPool.clear(); } + @override + void exercise() => run(); + @override void run() { final writers = []; @@ -174,6 +192,9 @@ class PoolStatisticsBenchmark extends BenchmarkBase { } } + @override + void exercise() => run(); + @override void run() { // Query statistics multiple times @@ -199,6 +220,9 @@ class PoolMixedOperationsBenchmark extends BenchmarkBase { BinaryWriterPool.clear(); } + @override + void exercise() => run(); + @override void run() { final batch1 = []; @@ -237,6 +261,9 @@ class PoolBufferReuseBenchmark extends BenchmarkBase { BinaryWriterPool.clear(); } + @override + void exercise() => run(); + @override void run() { // Use pool with varying write sizes @@ -266,6 +293,9 @@ class PoolResetStatisticsBenchmark extends BenchmarkBase { } } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { diff --git a/test/performance/writer/string_write_bench_test.dart b/test/performance/writer/string_write_bench_test.dart index fd87118..58e0783 100644 --- a/test/performance/writer/string_write_bench_test.dart +++ b/test/performance/writer/string_write_bench_test.dart @@ -13,6 +13,9 @@ class AsciiStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -33,6 +36,9 @@ class ShortAsciiStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 125; i++) { @@ -61,6 +67,9 @@ class LongAsciiStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { const longString = @@ -87,6 +96,9 @@ class CyrillicStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -107,6 +119,9 @@ class CjkStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -127,6 +142,9 @@ class EmojiStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -147,6 +165,9 @@ class MixedUnicodeStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -167,6 +188,9 @@ class VarStringAsciiWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -187,6 +211,9 @@ class VarStringMixedWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -207,6 +234,9 @@ class EmptyStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 8192); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -227,6 +257,9 @@ class RealisticMessageWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 100; i++) { @@ -258,6 +291,9 @@ class AlternatingStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { const shortString = 'Hi'; @@ -283,6 +319,9 @@ class VeryLongStringWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 65536); } + @override + void exercise() => run(); + @override void run() { final longString = 'Lorem ipsum dolor sit amet. ' * 80; diff --git a/test/performance/writer/varint_write_bench_test.dart b/test/performance/writer/varint_write_bench_test.dart index 7b9314d..709986d 100644 --- a/test/performance/writer/varint_write_bench_test.dart +++ b/test/performance/writer/varint_write_bench_test.dart @@ -13,6 +13,9 @@ class VarUintFastPathWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -33,6 +36,9 @@ class VarUint2ByteWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -54,6 +60,9 @@ class VarUint3ByteWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -75,6 +84,9 @@ class VarUint4ByteWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -95,6 +107,9 @@ class VarUint5ByteWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -115,6 +130,9 @@ class VarIntPositiveWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -135,6 +153,9 @@ class VarIntNegativeWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -155,6 +176,9 @@ class VarIntMixedWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 16384); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { @@ -176,6 +200,9 @@ class VarUintMixedSizesWriteBenchmark extends BenchmarkBase { writer = BinaryWriter(initialBufferSize: 32768); } + @override + void exercise() => run(); + @override void run() { for (var i = 0; i < 1000; i++) { diff --git a/test/unit/binary_reader_test.dart b/test/unit/binary_reader_test.dart index 914c605..8035658 100644 --- a/test/unit/binary_reader_test.dart +++ b/test/unit/binary_reader_test.dart @@ -6,7 +6,7 @@ import 'package:test/test.dart'; void main() { group('BinaryReader', () { - test('readUint8', () { + test('reads Uint8 correctly', () { final buffer = Uint8List.fromList([0x01]); final reader = BinaryReader(buffer); @@ -14,7 +14,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt8', () { + test('reads Int8 correctly', () { final buffer = Uint8List.fromList([0xFF]); final reader = BinaryReader(buffer); @@ -22,7 +22,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint16 big-endian', () { + test('reads Uint16 in big-endian', () { final buffer = Uint8List.fromList([0x01, 0x00]); final reader = BinaryReader(buffer); @@ -30,7 +30,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint16 little-endian', () { + test('reads Uint16 in little-endian', () { final buffer = Uint8List.fromList([0x00, 0x01]); final reader = BinaryReader(buffer); @@ -38,7 +38,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt16 big-endian', () { + test('reads Int16 in big-endian', () { final buffer = Uint8List.fromList([0xFF, 0xFF]); final reader = BinaryReader(buffer); @@ -46,7 +46,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt16 little-endian', () { + test('reads Int16 in little-endian', () { final buffer = Uint8List.fromList([0x00, 0x80]); final reader = BinaryReader(buffer); @@ -54,7 +54,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint32 big-endian', () { + test('reads Uint32 in big-endian', () { final buffer = Uint8List.fromList([0x00, 0x01, 0x00, 0x00]); final reader = BinaryReader(buffer); @@ -62,7 +62,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint32 little-endian', () { + test('reads Uint32 in little-endian', () { final buffer = Uint8List.fromList([0x00, 0x00, 0x01, 0x00]); final reader = BinaryReader(buffer); @@ -70,7 +70,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt32 big-endian', () { + test('reads Int32 in big-endian', () { final buffer = Uint8List.fromList([0xFF, 0xFF, 0xFF, 0xFF]); final reader = BinaryReader(buffer); @@ -78,7 +78,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt32 little-endian', () { + test('reads Int32 in little-endian', () { final buffer = Uint8List.fromList([0x00, 0x00, 0x00, 0x80]); final reader = BinaryReader(buffer); @@ -86,7 +86,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint64 big-endian', () { + test('reads Uint64 in big-endian', () { final buffer = Uint8List.fromList([ 0x00, 0x00, @@ -103,7 +103,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readUint64 little-endian', () { + test('reads Uint64 in little-endian', () { final buffer = Uint8List.fromList([ 0x00, 0x00, @@ -120,7 +120,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt64 big-endian', () { + test('reads Int64 in big-endian', () { final buffer = Uint8List.fromList([ 0xFF, 0xFF, @@ -137,7 +137,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readInt64 little-endian', () { + test('reads Int64 in little-endian', () { final buffer = Uint8List.fromList([ 0x00, 0x00, @@ -154,7 +154,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readFloat32 big-endian', () { + test('reads Float32 in big-endian', () { final buffer = Uint8List.fromList([0x40, 0x49, 0x0F, 0xDB]); // 3.1415927 final reader = BinaryReader(buffer); @@ -162,7 +162,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readFloat32 little-endian', () { + test('reads Float32 in little-endian', () { final buffer = Uint8List.fromList([0xDB, 0x0F, 0x49, 0x40]); // 3.1415927 final reader = BinaryReader(buffer); @@ -170,7 +170,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readFloat64 big-endian', () { + test('reads Float64 in big-endian', () { final buffer = Uint8List.fromList([ 0x40, 0x09, @@ -190,7 +190,7 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readFloat64 little-endian', () { + test('reads Float64 in little-endian', () { final buffer = Uint8List.fromList([ 0x18, 0x2D, @@ -274,31 +274,6 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readVarInt roundtrip with writeVarInt', () { - final writer = BinaryWriter() - ..writeVarUint(0) - ..writeVarUint(1) - ..writeVarUint(127) - ..writeVarUint(128) - ..writeVarUint(300) - ..writeVarUint(70000) - ..writeVarUint(1 << 20) - ..writeVarUint(1 << 30); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readVarUint(), equals(0)); - expect(reader.readVarUint(), equals(1)); - expect(reader.readVarUint(), equals(127)); - expect(reader.readVarUint(), equals(128)); - expect(reader.readVarUint(), equals(300)); - expect(reader.readVarUint(), equals(70000)); - expect(reader.readVarUint(), equals(1 << 20)); - expect(reader.readVarUint(), equals(1 << 30)); - expect(reader.availableBytes, equals(0)); - }); - test('readZigZag encoding for zero', () { final buffer = Uint8List.fromList([0]); final reader = BinaryReader(buffer); @@ -355,33 +330,6 @@ void main() { expect(reader.availableBytes, equals(0)); }); - test('readZigZag roundtrip with writeZigZag', () { - final writer = BinaryWriter() - ..writeVarInt(0) - ..writeVarInt(1) - ..writeVarInt(-1) - ..writeVarInt(2) - ..writeVarInt(-2) - ..writeVarInt(100) - ..writeVarInt(-100) - ..writeVarInt(2147483647) - ..writeVarInt(-2147483648); - - final buffer = writer.takeBytes(); - final reader = BinaryReader(buffer); - - expect(reader.readVarInt(), equals(0)); - expect(reader.readVarInt(), equals(1)); - expect(reader.readVarInt(), equals(-1)); - expect(reader.readVarInt(), equals(2)); - expect(reader.readVarInt(), equals(-2)); - expect(reader.readVarInt(), equals(100)); - expect(reader.readVarInt(), equals(-100)); - expect(reader.readVarInt(), equals(2147483647)); - expect(reader.readVarInt(), equals(-2147483648)); - expect(reader.availableBytes, equals(0)); - }); - test('readVarUint throws on truncated varint', () { // VarInt with continuation bit set but no following byte final buffer = Uint8List.fromList([0x80]); // MSB=1, expects more bytes @@ -407,7 +355,16 @@ void main() { ]); final reader = BinaryReader(buffer); - expect(reader.readVarUint, throwsA(isA())); + expect( + reader.readVarUint, + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('VarInt is too long'), + ), + ), + ); }); test('readVarInt throws on truncated zigzag', () { @@ -481,14 +438,6 @@ void main() { expect(reader.readUint8(), equals(0x02)); }); - test('read zero-length bytes', () { - final buffer = Uint8List.fromList([]); - final reader = BinaryReader(buffer); - - expect(reader.readBytes(0), equals([])); - expect(reader.availableBytes, equals(0)); - }); - test('read beyond buffer throws RangeError', () { final buffer = Uint8List.fromList([0x01, 0x02]); final reader = BinaryReader(buffer); @@ -1916,6 +1865,22 @@ void main() { expect(reader.readVarUint(), equals(0x1FFFFF)); expect(reader.readVarUint(), equals(0x200000)); }); + + test('readVarInt throws on value exceeding int64 range', () { + // Create buffer with VarInt that would decode to value > max int64 + // This tests overflow protection + final buffer = Uint8List.fromList([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // + 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, // Maximum valid VarInt encoding + ]); + final reader = BinaryReader(buffer); + + // Should successfully read maximum value without throwing + expect( + reader.readVarInt, + returnsNormally, + ); + }); }); group('VarBytes/VarString error handling', () { diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index d765d18..7159873 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -12,7 +12,7 @@ void main() { writer = BinaryWriter(); }); - test('throw RangeError when initialBufferSize is not positive', () { + test('throws RangeError when initialBufferSize is not positive', () { expect( () => BinaryWriter(initialBufferSize: 0), throwsA( @@ -21,171 +21,171 @@ void main() { ); }); - test('return empty list when takeBytes called on empty writer', () { + test('returns empty list when takeBytes called on empty writer', () { expect(writer.takeBytes(), isEmpty); }); - test('write single Uint8 value correctly', () { + test('writes single Uint8 value correctly', () { writer.writeUint8(1); expect(writer.takeBytes(), [1]); }); - test('write negative Int8 value correctly', () { + test('writes negative Int8 value correctly', () { writer.writeInt8(-1); expect(writer.takeBytes(), [255]); }); - test('write Uint16 in big-endian format', () { + test('writes Uint16 in big-endian format', () { writer.writeUint16(256); expect(writer.takeBytes(), [1, 0]); }); - test('write Uint16 in little-endian format', () { + test('writes Uint16 in little-endian format', () { writer.writeUint16(256, .little); expect(writer.takeBytes(), [0, 1]); }); - test('write Int16 in big-endian format', () { + test('writes Int16 in big-endian format', () { writer.writeInt16(-1); expect(writer.takeBytes(), [255, 255]); }); - test('write Int16 in little-endian format', () { + test('writes Int16 in little-endian format', () { writer.writeInt16(-32768, .little); expect(writer.takeBytes(), [0, 128]); }); - test('write Uint32 in big-endian format', () { + test('writes Uint32 in big-endian format', () { writer.writeUint32(65536); expect(writer.takeBytes(), [0, 1, 0, 0]); }); - test('write Uint32 in little-endian format', () { + test('writes Uint32 in little-endian format', () { writer.writeUint32(65536, .little); expect(writer.takeBytes(), [0, 0, 1, 0]); }); - test('write Int32 in big-endian format', () { + test('writes Int32 in big-endian format', () { writer.writeInt32(-1); expect(writer.takeBytes(), [255, 255, 255, 255]); }); - test('write Int32 in little-endian format', () { + test('writes Int32 in little-endian format', () { writer.writeInt32(-2147483648, .little); expect(writer.takeBytes(), [0, 0, 0, 128]); }); - test('write Uint64 in big-endian format', () { + test('writes Uint64 in big-endian format', () { writer.writeUint64(4294967296); expect(writer.takeBytes(), [0, 0, 0, 1, 0, 0, 0, 0]); }); - test('write Uint64 in little-endian format', () { + test('writes Uint64 in little-endian format', () { writer.writeUint64(4294967296, .little); expect(writer.takeBytes(), [0, 0, 0, 0, 1, 0, 0, 0]); }); - test('write Int64 in big-endian format', () { + test('writes Int64 in big-endian format', () { writer.writeInt64(-1); expect(writer.takeBytes(), [255, 255, 255, 255, 255, 255, 255, 255]); }); - test('write Int64 in little-endian format', () { + test('writes Int64 in little-endian format', () { writer.writeInt64(-9223372036854775808, .little); expect(writer.takeBytes(), [0, 0, 0, 0, 0, 0, 0, 128]); }); - test('write Float32 in big-endian format', () { + test('writes Float32 in big-endian format', () { writer.writeFloat32(3.1415927); expect(writer.takeBytes(), [64, 73, 15, 219]); }); - test('write Float32 in little-endian format', () { + test('writes Float32 in little-endian format', () { writer.writeFloat32(3.1415927, .little); expect(writer.takeBytes(), [219, 15, 73, 64]); }); - test('write Float64 in big-endian format', () { + test('writes Float64 in big-endian format', () { writer.writeFloat64(3.141592653589793); expect(writer.takeBytes(), [64, 9, 33, 251, 84, 68, 45, 24]); }); - test('write Float64 in little-endian format', () { + test('writes Float64 in little-endian format', () { writer.writeFloat64(3.141592653589793, .little); expect(writer.takeBytes(), [24, 45, 68, 84, 251, 33, 9, 64]); }); - test('write VarInt single byte (0)', () { + test('writes VarInt single byte (0)', () { writer.writeVarUint(0); expect(writer.takeBytes(), [0]); }); - test('write VarInt single byte (127)', () { + test('writes VarInt single byte (127)', () { writer.writeVarUint(127); expect(writer.takeBytes(), [127]); }); - test('write VarInt two bytes (128)', () { + test('writes VarInt two bytes (128)', () { writer.writeVarUint(128); expect(writer.takeBytes(), [0x80, 0x01]); }); - test('write VarInt two bytes (300)', () { + test('writes VarInt two bytes (300)', () { writer.writeVarUint(300); expect(writer.takeBytes(), [0xAC, 0x02]); }); - test('write VarInt three bytes (16384)', () { + test('writes VarInt three bytes (16384)', () { writer.writeVarUint(16384); expect(writer.takeBytes(), [0x80, 0x80, 0x01]); }); - test('write VarInt four bytes (2097151)', () { + test('writes VarInt four bytes (2097151)', () { writer.writeVarUint(2097151); expect(writer.takeBytes(), [0xFF, 0xFF, 0x7F]); }); - test('write VarInt five bytes (268435455)', () { + test('writes VarInt five bytes (268435455)', () { writer.writeVarUint(268435455); expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0x7F]); }); - test('write VarInt large value', () { + test('writes VarInt large value', () { writer.writeVarUint(1 << 30); expect(writer.takeBytes(), [0x80, 0x80, 0x80, 0x80, 0x04]); }); - test('write ZigZag encoding for positive values', () { + test('writes ZigZag encoding for positive values', () { writer.writeVarInt(0); expect(writer.takeBytes(), [0]); }); - test('write ZigZag encoding for positive value 1', () { + test('writes ZigZag encoding for positive value 1', () { writer.writeVarInt(1); expect(writer.takeBytes(), [2]); }); - test('write ZigZag encoding for negative value -1', () { + test('writes ZigZag encoding for negative value -1', () { writer.writeVarInt(-1); expect(writer.takeBytes(), [1]); }); - test('write ZigZag encoding for positive value 2', () { + test('writes ZigZag encoding for positive value 2', () { writer.writeVarInt(2); expect(writer.takeBytes(), [4]); }); - test('write ZigZag encoding for negative value -2', () { + test('writes ZigZag encoding for negative value -2', () { writer.writeVarInt(-2); expect(writer.takeBytes(), [3]); }); - test('write ZigZag encoding for large positive value', () { + test('writes ZigZag encoding for large positive value', () { writer.writeVarInt(2147483647); expect(writer.takeBytes(), [0xFE, 0xFF, 0xFF, 0xFF, 0x0F]); }); - test('write ZigZag encoding for large negative value', () { + test('writes ZigZag encoding for large negative value', () { writer.writeVarInt(-2147483648); expect(writer.takeBytes(), [0xFF, 0xFF, 0xFF, 0xFF, 0x0F]); }); @@ -1570,17 +1570,6 @@ void main() { expect(reader.readVarBytes(), equals([3, 4, 5])); expect(reader.readVarBytes(), equals([6])); }); - - test('writeVarBytes round-trip', () { - final writer = BinaryWriter(); - final original = List.generate(256, (i) => i); - writer.writeVarBytes(original); - - final reader = BinaryReader(writer.takeBytes()); - final result = reader.readVarBytes(); - - expect(result, equals(original)); - }); }); group('VarString operations', () { @@ -1626,17 +1615,6 @@ void main() { expect(bytes.length, equals(16)); // 1 + 15 }); - test('writeVarString round-trip with reader', () { - final writer = BinaryWriter(); - const testString = 'Test 测试 🎉'; - writer.writeVarString(testString); - - final reader = BinaryReader(writer.toBytes()); - final result = reader.readVarString(); - - expect(result, equals(testString)); - }); - test('writeVarString with malformed handling', () { final writer = BinaryWriter(); // Lone high surrogate (U+D800) From 09e941e92995a156a3a885dd7f7a94bdc0f9ab12 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Wed, 7 Jan 2026 13:10:48 +0200 Subject: [PATCH 26/29] Enhance BinaryWriter: Add capacity management features and tests for buffer alignment and expansion --- README.md | 314 +++++++++++++++++++++++++++--- lib/src/binary_writer.dart | 25 ++- test/unit/binary_writer_test.dart | 194 ++++++++++++++++++ 3 files changed, 493 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 08eba0b..91d696f 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,17 @@ [![Tests](https://github.com/pro100andrey/pro_binary/workflows/Tests/badge.svg)](https://github.com/pro100andrey/pro_binary/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -High-performance binary serialization for Dart. Fast, type-safe, and easy to use. +High-performance binary serialization library for Dart with zero-copy operations, efficient memory management, and Protocol Buffers-compatible VarInt encoding. -## Why pro_binary? +## Features -- 🚀 **Fast**: Optimized for performance with zero-copy operations -- 🎯 **Type-safe**: Full support for all Dart primitive types -- 🔍 **Developer-friendly**: Clear error messages in debug mode -- 📦 **Smart**: Auto-expanding buffers, VarInt encoding for smaller payloads -- 🌐 **Flexible**: Big-endian and little-endian support +- 🚀 **Zero-copy reads**: Direct `Uint8List` views without data duplication +- ⚡ **Optimized writes**: Exponential buffer growth strategy (×1.5) with pooling support +- 🔢 **VarInt encoding**: Protocol Buffers-compatible variable-length integer encoding +- 🎯 **Type-safe API**: Full support for all Dart primitive types (int8-int64, float32/64, bool) +- 🌐 **Endianness support**: Both big-endian (default) and little-endian byte order +- 📦 **Memory efficient**: Automatic buffer management with configurable initial capacity +- 🧪 **Battle-tested**: 556+ tests with extensive edge case coverage ## Installation @@ -123,10 +125,14 @@ final text = 'Hello, 世界! 🌍'; final encoded = utf8.encode(text); writer.writeVarUint(encoded.length); writer.writeString(text); +// or simply +writer.writeVarString(text); // Read final length = reader.readVarUint(); final text = reader.readString(length); +// or simply +final text = reader.readVarString(); ``` ### Struct-like Data @@ -157,6 +163,67 @@ class Player { } ``` +## Architecture + +### BinaryWriter + +```dart +final writer = BinaryWriter(initialBufferSize: 128); // Default: 128 bytes +``` + +**Buffer Management:** + +- Initial capacity: 128 bytes (configurable) +- Growth strategy: `newCapacity = max(currentCapacity * 1.5, currentCapacity + requiredBytes)` with 64-byte alignment +- Resets buffer without reallocation: `writer.reset()` +- Takes ownership of buffer: `writer.takeBytes()` (one-time use, resets writer) +- Creates view without reset: `writer.toBytes()` (reusable) + +**Write Operations:** + +- Fixed-width integers: `writeUint8`, `writeInt16`, `writeUint32`, `writeInt64`, etc. +- Variable-length integers: `writeVarUint` (unsigned), `writeVarInt` (ZigZag-encoded signed) +- Floating-point: `writeFloat32`, `writeFloat64` +- Binary data: `writeBytes`, `writeVarBytes` (length-prefixed) +- Strings: `writeString` (raw UTF-8), `writeVarString` (length-prefixed) +- Boolean: `writeBool` (1 byte: 0x00 or 0x01) + +### BinaryReader + +```dart +final reader = BinaryReader(bytes); +``` + +**Zero-Copy Design:** + +- No buffer copying: operates on `Uint8List.view` of input data +- Direct memory access via `ByteData` for endianness handling +- Automatic offset tracking with bounds checking + +**Read Operations:** + +- Fixed-width integers: `readUint8`, `readInt16`, `readUint32`, `readInt64`, etc. +- Variable-length integers: `readVarUint`, `readVarInt` (ZigZag-decoded) +- Floating-point: `readFloat32`, `readFloat64` +- Binary data: `readBytes`, `readVarBytes`, `readRemainingBytes` +- Strings: `readString` (raw UTF-8), `readVarString` (length-prefixed) +- Boolean: `readBool` + +**Navigation API:** + +- `skip(int bytes)`: Move forward by N bytes +- `seek(int position)`: Jump to absolute position +- `rewind(int bytes)`: Move backward by N bytes +- `reset()`: Return to start +- `peekBytes(int length, [int offset])`: Look ahead without consuming +- `hasBytes(int count)`: Check if enough bytes available + +**State Inspection:** + +- `offset`: Current read position (0-based) +- `length`: Total buffer size +- `availableBytes`: Remaining unread bytes + ## VarInt Encoding VarInt uses fewer bytes for small numbers: @@ -170,6 +237,14 @@ writer.writeVarInt(-1); // 1 byte (ZigZag encoded) writer.writeVarInt(-1000); // 2 bytes ``` +**Implementation Details:** + +- Protocol Buffers Base 128 Varint encoding +- 7 data bits + 1 continuation bit per byte +- Maximum 10 bytes for 64-bit values +- ZigZag encoding for signed integers: `(n << 1) ^ (n >> 63)` +- Fast path optimization for single-byte values (0-127) + **Use VarUint** for: lengths, counts, IDs **Use VarInt** for: deltas, offsets, signed values @@ -193,65 +268,242 @@ VarInt encoding significantly reduces payload size for small values: ## Tips & Best Practices -**Buffer Sizing**: Writer starts at 128 bytes and auto-expands. For large data, set initial size: +### Performance Optimization + +**Pre-allocate buffers** for known data sizes: ```dart +// For ~1KB messages final writer = BinaryWriter(initialBufferSize: 1024); + +// Avoid multiple small allocations +final writer = BinaryWriter(initialBufferSize: 8192); // For bulk writes +``` + +**Use object pooling** for high-frequency operations: + +```dart +// Acquire from pool +final writer = BinaryWriterPool.acquire(); +try { + writer.writeUint32(value); + final bytes = writer.takeBytes(); + send(bytes); +} finally { + // Return to pool for reuse + BinaryWriterPool.release(writer); +} + +// Pool statistics +print(BinaryWriterPool.poolSize); // Available writers +print(BinaryWriterPool.peakPoolSize); // High water mark +``` + +**Choose correct integer type**: + +```dart +// VarInt for small values (lengths, counts) +writer.writeVarUint(items.length); // 1 byte for length < 128 + +// Fixed-width for large/unpredictable values +writer.writeUint32(timestamp); // Always 4 bytes, predictable +writer.writeUint64(uuid); // Fixed 8 bytes ``` -**Endianness**: Defaults to big-. Specify when needed: +### Endianness + +Default: **big-endian** (network byte order). Specify when needed: ```dart +// Explicit endianness writer.writeUint32(value, .little); +writer.writeFloat64(3.14, .big); + +// Reading must match writing +final value = reader.readUint32(.little); +``` + +**When to use little-endian:** + +- Interop with x86/ARM systems (native byte order) +- Matching existing binary formats (e.g., RIFF, BMP) +- Performance-critical code on little-endian CPUs + +### String Encoding + +Always use **length-prefixed** encoding for variable-length strings: + +```dart +// ✅ Good: Self-describing +writer.writeVarString('Hello'); +// Equivalent to: +// writer.writeVarUint(utf8.encode('Hello').length); +// writer.writeString('Hello'); + +// ❌ Bad: No way to determine string boundaries +writer.writeString('Hello'); +writer.writeString('World'); // Where does first string end? ``` -**String Encoding**: Always use length-prefix for variable strings: +For **fixed-length** strings, calculate UTF-8 byte length: ```dart -// ✅ Good +final text = 'Hello, 世界!'; final bytes = utf8.encode(text); -writer.writeVarUint(bytes.length); +writer.writeUint16(bytes.length); // Store byte length writer.writeString(text); -// ❌ Bad - no way to know where string ends -writer.writeString(text); +// Reading +final byteLength = reader.readUint16(); +final text = reader.readString(byteLength); ``` -**Error Handling**: Invalid data and out-of-bounds reads/writes throw `RangeError`. Catch errors for user input: +### Error Handling + +All operations throw `RangeError` on invalid data or buffer overflow: ```dart +// Buffer underflow +try { + final value = reader.readUint32(); // Not enough bytes +} on RangeError catch (e) { + print('Buffer underflow: $e'); +} + +// Invalid VarInt try { - final value = reader.readUint32(); -} catch (e) { - print('Invalid data: $e'); + final value = reader.readVarInt(); // Malformed encoding +} on FormatException catch (e) { + print('Invalid VarInt: $e'); +} + +// String decoding errors +try { + final text = reader.readString(10, allowMalformed: false); +} on FormatException catch (e) { + print('Invalid UTF-8: $e'); +} +``` + +### Design Patterns + +**Tagged unions** (discriminated unions): + +```dart +enum MessageType { ping, data, ack } + +void writeMessage(BinaryWriter w, MessageType type, dynamic payload) { + w.writeUint8(type.index); + switch (type) { + case MessageType.ping: + // No payload + break; + case MessageType.data: + w.writeVarBytes(payload as List); + break; + case MessageType.ack: + w.writeUint32(payload as int); // Sequence number + break; + } +} +``` + +**Version-tolerant serialization**: + +```dart +class Message { + static const int version = 2; + + void writeTo(BinaryWriter w) { + w.writeUint8(version); // Version byte + w.writeVarUint(id); // Field 1 + w.writeVarString(text); // Field 2 + // Version 2: added timestamp + if (version >= 2) { + w.writeUint64(timestamp); + } + } + + static Message readFrom(BinaryReader r) { + final ver = r.readUint8(); + final id = r.readVarUint(); + final text = r.readVarString(); + final timestamp = ver >= 2 ? r.readUint64() : 0; + return Message(id, text, timestamp); + } } ``` ## Testing -Comprehensive test suite with **336+ tests** covering: +Comprehensive test suite with **556 tests** covering: + +- ✅ **Unit tests (417)**: Isolated BinaryReader/Writer method testing + - All primitive types (int8-int64, float32/64, bool) + - VarInt/VarUint encoding/decoding (70+ dedicated tests) + - Boundary conditions and overflow detection + - UTF-8 handling (multi-byte chars, emojis, malformed sequences) + - Navigation API (seek, skip, rewind, peek) + - Error handling and exception cases + +- ✅ **Integration tests (92)**: End-to-end roundtrip validation + - Write → Read consistency for all data types + - Buffer expansion under load + - Complex data structure serialization -- ✅ **VarInt/VarUint encoding** - 70+ dedicated tests for variable-length integers -- ✅ **All data types** - Exhaustive testing of read/write operations -- ✅ **Edge cases** - Boundary conditions, overflow, special values -- ✅ **UTF-8 handling** - Multi-byte characters, emojis, malformed sequences -- ✅ **Round-trip validation** - Ensures data integrity through encode/decode cycles -- ✅ **Performance benchmarks** - Tracks optimization effectiveness +- ✅ **Performance benchmarks (51)**: Optimization tracking + - Read/write throughput for all operations + - Buffer growth patterns + - VarInt encoding efficiency by value range + - Navigation operation costs Run tests: ```bash -dart test -x benchmark # Run unit/integration tests (skip benchmarks) -dart test -t benchmark # Run performance benchmarks only -dart test # Run everything (including benchmarks) -dart test test/binary_reader_test.dart # Run a single test file -dart analyze # Check code quality +# Run unit + integration tests (skip benchmarks) +dart test -x benchmark + +# Run performance benchmarks only +dart test -t benchmark + +# Run all tests including benchmarks +dart test + +# Run specific test file +dart test test/unit/binary_reader_test.dart + +# Run with coverage +dart pub global activate coverage +dart pub global run coverage:test_with_coverage -- -x benchmark + +# Code analysis +dart analyze --fatal-infos +dart format --set-exit-if-changed . ``` ## Contributing -Found a bug or have a feature idea? [Open an issue](https://github.com/pro100andrey/pro_binary/issues) or submit a PR! +Contributions are welcome! Please: + +1. **Open an issue** first to discuss major changes +2. **Follow existing code style** (run `dart format`) +3. **Add tests** for new features (maintain >95% coverage) +4. **Update documentation** including README examples +5. **Run full test suite** before submitting PR + + ```bash + dart analyze --fatal-infos + dart format --set-exit-if-changed . + dart test + ``` + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed guidelines. ## License MIT License - see [LICENSE](./LICENSE) for details. + +--- + +Need help? Found a bug? Have a feature request? +👉 [Open an issue](https://github.com/pro100andrey/pro_binary/issues) diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 16109b9..080f281 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -14,7 +14,6 @@ import 'dart:typed_data'; /// Example: /// ```dart /// final writer = BinaryWriter(); -/// /// // Write various data types /// writer.writeUint32(42); /// writer.writeFloat64(3.14); @@ -41,6 +40,9 @@ extension type BinaryWriter._(_WriterState _ws) { /// Returns the total number of bytes written to the buffer. int get bytesWritten => _ws.offset; + /// Returns the current capacity of the internal buffer. + int get capacity => _ws.capacity; + /// Writes an unsigned variable-length integer using VarInt encoding. /// /// VarInt encoding uses the lower 7 bits of each byte for data and the @@ -661,10 +663,11 @@ final class _WriterState { _WriterState(int initialBufferSize) : this._validated(_validateInitialBufferSize(initialBufferSize)); - _WriterState._validated(this._size) - : capacity = _size, + _WriterState._validated(int size) + : _size = size, + capacity = (size + 63) & ~63, offset = 0, - list = Uint8List(_size) { + list = Uint8List((size + 63) & ~63) { data = list.buffer.asByteData(); } @@ -698,9 +701,10 @@ final class _WriterState { @pragma('vm:prefer-inline') void _initializeBuffer() { - list = Uint8List(_size); + final alignedSize = (_size + 63) & ~63; + list = Uint8List(alignedSize); data = list.buffer.asByteData(); - capacity = _size; + capacity = alignedSize; offset = 0; } @@ -752,16 +756,19 @@ final class _WriterState { /// Expands the buffer to accommodate additional data. /// - /// Uses exponential growth (2x) for better amortized performance, + /// Uses exponential growth (1.5x) for better memory efficiency, /// but ensures the buffer is always large enough for the requested size. void _expand(int size) { final req = offset + size; - // Double the capacity (exponential growth) - var newCapacity = capacity * 2; + // Grow by 1.5x (exponential growth with better memory efficiency) + var newCapacity = capacity + (capacity >> 1); + // Ensure we meet the minimum requirement if (newCapacity < req) { newCapacity = req; } + // Align to 64-byte boundary + newCapacity = (newCapacity + 63) & ~63; list = Uint8List(newCapacity)..setRange(0, offset, list); diff --git a/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index 7159873..95586d8 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -369,6 +369,200 @@ void main() { expect(writer.bytesWritten, equals(10007)); }); + test('initial capacity is 128 bytes by default and aligned', () { + expect(writer.capacity, equals(128)); + expect(writer.capacity % 64, equals(0)); + }); + + test('capacity is aligned to 64-byte boundary on initialization', () { + // Test various sizes + final customWriter256 = BinaryWriter(initialBufferSize: 256); + expect(customWriter256.capacity, equals(256)); + expect(customWriter256.capacity % 64, equals(0)); + + // Size 50 should be aligned to 64 + final customWriter50 = BinaryWriter(initialBufferSize: 50); + expect(customWriter50.capacity, equals(64)); + expect(customWriter50.capacity % 64, equals(0)); + + // Size 100 should be aligned to 128 + final customWriter100 = BinaryWriter(initialBufferSize: 100); + expect(customWriter100.capacity, equals(128)); + expect(customWriter100.capacity % 64, equals(0)); + }); + + test('capacity increases after buffer expansion', () { + // Default capacity is 128 bytes + expect(writer.capacity, equals(128)); + + // Write data that exceeds initial capacity + final largeData = Uint8List(200); + writer.writeBytes(largeData); + + // Capacity with 1.5x growth: need 200, 128 * 1.5 = 192 < 200, so use + // 200 aligned to 256 + expect(writer.capacity, equals(256)); + }); + + test('capacity expands with 1.5x growth strategy', () { + final smallWriter = BinaryWriter(initialBufferSize: 64); + expect(smallWriter.capacity, equals(64)); + + // Write 100 bytes (exceeds initial 64) + // 64 * 1.5 = 96 < 100, so use 100 aligned to 128 + smallWriter.writeBytes(Uint8List(100)); + + expect(smallWriter.capacity, equals(128)); + }); + + test('capacity resets to initial size after reset', () { + // Force expansion + writer.writeBytes(Uint8List(200)); + expect(writer.capacity, greaterThan(128)); + + // reset() should reset capacity back to initial size (128) + writer.reset(); + expect(writer.capacity, equals(128)); + expect(writer.bytesWritten, equals(0)); + }); + + test('capacity resets to initial size after takeBytes', () { + // Force expansion + writer.writeBytes(Uint8List(200)); + expect(writer.capacity, greaterThan(128)); + + // takeBytes() resets to initial size (128) + writer.takeBytes(); + expect(writer.capacity, equals(128)); + expect(writer.bytesWritten, equals(0)); + }); + + test('capacity does not change with toBytes', () { + writer.writeBytes(Uint8List(200)); + final capacityBefore = writer.capacity; + + // toBytes() should not change capacity + final bytes = writer.toBytes(); + expect(writer.capacity, equals(capacityBefore)); + expect(bytes.length, equals(200)); + }); + + test('capacity aligns to 64-byte boundary after expansion', () { + // Start with 128 bytes (already aligned to 64) + expect(writer.capacity, equals(128)); + expect(writer.capacity % 64, equals(0)); + + // Write 200 bytes -> requires 256 capacity (128 * 2) + // 256 is already aligned to 64, so capacity should be 256 + writer.writeBytes(Uint8List(200)); + expect(writer.capacity, equals(256)); + expect(writer.capacity % 64, equals(0)); + }); + + test('capacity aligns to 64-byte boundary from small initial size', () { + // Start with 50 bytes -> aligned to 64 + final smallWriter = BinaryWriter(initialBufferSize: 50); + expect(smallWriter.capacity, equals(64)); + + // Write 100 bytes -> 64 * 2 = 128 (aligned) + smallWriter.writeBytes(Uint8List(100)); + expect(smallWriter.capacity, equals(128)); + expect(smallWriter.capacity % 64, equals(0)); + }); + + test('capacity alignment happens on initialization and expansion', () { + // Test that both initialization and expansion align to 64-byte boundary + final sizes = [1, 17, 33, 65, 99, 130]; + final expectedInitial = [64, 64, 64, 128, 128, 192]; + + for (var i = 0; i < sizes.length; i++) { + final size = sizes[i]; + final expected = expectedInitial[i]; + final w = BinaryWriter(initialBufferSize: size); + + // Initial capacity should be aligned + expect( + w.capacity, + equals(expected), + reason: 'Initial size $size should align to $expected', + ); + expect( + w.capacity % 64, + equals(0), + reason: 'Initial capacity should be aligned', + ); + + // After expansion, capacity should still be aligned + w.writeBytes(Uint8List(w.capacity + 1)); + expect( + w.capacity % 64, + equals(0), + reason: 'Capacity after expansion should be aligned', + ); + } + }); + + test('capacity expansion maintains 64-byte alignment', () { + // Start with 64 bytes + final w = BinaryWriter(initialBufferSize: 64); + expect(w.capacity, equals(64)); + + // Force multiple expansions + w.writeBytes( + Uint8List(100), + ); // Need 100, 64 * 1.5 = 96 < 100, so use 100 aligned to 128 + expect(w.capacity % 64, equals(0)); + expect(w.capacity, equals(128)); + // Total 250, need 250: 128 * 1.5 = 192 < 250, so use 250 aligned to 256 + w.writeBytes(Uint8List(150)); + expect(w.capacity % 64, equals(0)); + expect(w.capacity, equals(256)); + }); + + test('capacity with exact requirement uses alignment', () { + final w = BinaryWriter(initialBufferSize: 64); + expect(w.capacity, equals(64)); // Already aligned + + // Write exactly 65 bytes -> need 65 total capacity + // Current: 64, need: 65, so expand: 64 * 1.5 = 96, aligned to 64 = 128 + w.writeBytes(Uint8List(65)); + expect(w.capacity, equals(128)); + expect(w.capacity % 64, equals(0)); + + // Now write 65 more bytes -> total written: 130, need 130 capacity + // Current: 128, need: 130, so expand: 128 * 1.5 = 192 (already aligned) + w.writeBytes(Uint8List(65)); + expect(w.capacity, equals(192)); + expect(w.capacity % 64, equals(0)); + }); + + test('capacity alignment calculation is correct', () { + // Test specific alignment calculations + final testCases = { + 1: 64, // (1 + 63) & ~63 = 64 + 63: 64, // (63 + 63) & ~63 = 64 + 64: 64, // (64 + 63) & ~63 = 64 + 65: 128, // (65 + 63) & ~63 = 128 + 127: 128, // (127 + 63) & ~63 = 128 + 128: 128, // (128 + 63) & ~63 = 128 + 129: 192, // (129 + 63) & ~63 = 192 + 255: 256, // (255 + 63) & ~63 = 256 + 256: 256, // (256 + 63) & ~63 = 256 + 257: 320, // (257 + 63) & ~63 = 320 + }; + + for (final entry in testCases.entries) { + final unaligned = entry.key; + final aligned = entry.value; + final calculated = (unaligned + 63) & ~63; + expect( + calculated, + equals(aligned), + reason: 'Alignment of $unaligned should be $aligned', + ); + } + }); + group('Input validation', () { test('throw RangeError when Uint8 value is negative', () { expect( From 7fae21ea78ecd55237fc4c39573390396d34c6e9 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 8 Jan 2026 10:27:46 +0200 Subject: [PATCH 27/29] Add constants for max and min Int64 values for native and web platforms --- lib/src/binary_reader.dart | 29 +++++++++++++++++++++++++ lib/src/binary_writer.dart | 41 ++++++++++++++++++++++++++++++----- lib/src/constants_native.dart | 2 ++ lib/src/constants_web.dart | 2 ++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 lib/src/constants_native.dart create mode 100644 lib/src/constants_web.dart diff --git a/lib/src/binary_reader.dart b/lib/src/binary_reader.dart index 0e59057..0d884a7 100644 --- a/lib/src/binary_reader.dart +++ b/lib/src/binary_reader.dart @@ -33,14 +33,17 @@ extension type const BinaryReader._(_ReaderState _rs) { /// Returns the number of bytes remaining to be read. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int get availableBytes => _rs.length - _rs.offset; /// Returns the current read position in the buffer. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int get offset => _rs.offset; /// Returns the total length of the buffer in bytes. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int get length => _rs.length; /// Reads an unsigned variable-length integer encoded using VarInt format. @@ -70,6 +73,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// Throws [FormatException] if the VarInt exceeds 10 bytes (malformed data). /// Asserts bounds in debug mode if attempting to read past buffer end. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readVarUint() { final list = _rs.list; final len = _rs.length; @@ -130,6 +134,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// final position = lastPosition + delta; /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readVarInt() { final v = readVarUint(); // Decode: right shift by 1, XOR with sign-extended LSB @@ -145,6 +150,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readUint8() { _checkBounds(1, 'Uint8'); @@ -160,6 +166,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readInt8() { _checkBounds(1, 'Int8'); @@ -177,6 +184,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readUint16([Endian endian = .big]) { _checkBounds(2, 'Uint16'); @@ -197,6 +205,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readInt16([Endian endian = .big]) { _checkBounds(2, 'Int16'); @@ -217,6 +226,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readUint32([Endian endian = .big]) { _checkBounds(4, 'Uint32'); @@ -236,6 +246,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readInt32([Endian endian = .big]) { _checkBounds(4, 'Int32'); final value = _rs.data.getInt32(_rs.offset, endian); @@ -259,6 +270,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readUint64([Endian endian = .big]) { _checkBounds(8, 'Uint64'); final value = _rs.data.getUint64(_rs.offset, endian); @@ -279,6 +291,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int readInt64([Endian endian = .big]) { _checkBounds(8, 'Int64'); final value = _rs.data.getInt64(_rs.offset, endian); @@ -297,6 +310,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') double readFloat32([Endian endian = .big]) { _checkBounds(4, 'Float32'); @@ -317,6 +331,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') double readFloat64([Endian endian = .big]) { _checkBounds(8, 'Float64'); @@ -342,6 +357,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') Uint8List readBytes(int length) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); @@ -369,6 +385,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// print('Payload length: ${payload.length}'); /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') Uint8List readRemainingBytes() => readBytes(availableBytes); /// Reads a length-prefixed byte array. @@ -394,6 +411,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') Uint8List readVarBytes() { final length = readVarUint(); return readBytes(length); @@ -421,6 +439,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// **Performance:** Zero-copy operation using buffer views. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') String readString(int length, {bool allowMalformed = false}) { if (length == 0) { return ''; @@ -454,6 +473,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Throws [RangeError] if attempting to read past buffer end. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') String readVarString({bool allowMalformed = false}) { final length = readVarUint(); return readString(length, allowMalformed: allowMalformed); @@ -469,6 +489,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// ``` /// Asserts bounds in debug mode if insufficient bytes are available. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') bool readBool() { final value = readUint8(); return value != 0; @@ -489,6 +510,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// } /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') bool hasBytes(int length) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); @@ -517,6 +539,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// final actualType = reader.readUint8(); // Now read it /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') Uint8List peekBytes(int length, [int? offset]) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); @@ -549,6 +572,8 @@ extension type const BinaryReader._(_ReaderState _rs) { /// final payloadSize = reader.readUint32(); /// reader.skip(payloadSize); /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void skip(int length) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); @@ -570,6 +595,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// final value = reader.readUint32(); /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void seek(int position) { if (position < 0 || position > _rs.length) { throw RangeError.range(position, 0, _rs.length, 'position'); @@ -589,6 +615,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// final value = reader.readUint32(); /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void rewind(int length) { if (length < 0) { throw RangeError.value(length, 'length', 'Length must be non-negative'); @@ -605,6 +632,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// This allows re-reading the same data without creating a new reader. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void reset() { _rs.offset = 0; } @@ -613,6 +641,7 @@ extension type const BinaryReader._(_ReaderState _rs) { /// /// Throws an assertion error in debug mode if not enough bytes. @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'); diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 080f281..91005a8 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -1,5 +1,9 @@ import 'dart:typed_data'; +// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER +// for explanation of max safe integer in JavaScript. +import 'constants_native.dart' if (dart.library.js_util) 'constants_web.dart'; + /// A high-performance binary writer for encoding data into a byte buffer. /// /// Provides methods for writing various data types including: @@ -66,7 +70,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeVarUint(1000000); // 3 bytes /// ``` @pragma('vm:prefer-inline') - @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeVarUint(int value) { // Fast path: single-byte (0-127) var offset = _ws.offset; @@ -147,6 +151,8 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeVarInt(42); // 1 byte /// writer.writeVarInt(-42); // 1 byte /// ``` + @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeVarInt(int value) { // ZigZag: (n << 1) ^ (n >> 63) // Maps: 0=>0, -1=>1, 1=>2, -2=>3, 2=>4, -3=>5, 3=>6 @@ -163,6 +169,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeUint8(int value) { _checkRange(value, 0, 255, 'Uint8'); _ws.ensureOneByte(); @@ -179,6 +186,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeInt8(int value) { _checkRange(value, -128, 127, 'Int8'); _ws.ensureOneByte(); @@ -197,6 +205,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeUint16(int value, [Endian endian = .big]) { _checkRange(value, 0, 65535, 'Uint16'); _ws.ensureTwoBytes(); @@ -216,6 +225,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeInt16(int value, [Endian endian = .big]) { _checkRange(value, -32768, 32767, 'Int16'); _ws.ensureTwoBytes(); @@ -235,6 +245,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeUint32(int value, [Endian endian = .big]) { _checkRange(value, 0, 4294967295, 'Uint32'); _ws.ensureFourBytes(); @@ -254,6 +265,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeInt32(int value, [Endian endian = .big]) { _checkRange(value, -2147483648, 2147483647, 'Int32'); _ws.ensureFourBytes(); @@ -279,8 +291,9 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeUint64(int value, [Endian endian = .big]) { - _checkRange(value, 0, 9223372036854775807, 'Uint64'); + _checkRange(value, 0, kMaxInt64, 'Uint64'); _ws.ensureEightBytes(); _ws.data.setUint64(_ws.offset, value, endian); @@ -300,8 +313,9 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// Throws [RangeError] if [value] is outside the valid range. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeInt64(int value, [Endian endian = .big]) { - _checkRange(value, -9223372036854775808, 9223372036854775807, 'Int64'); + _checkRange(value, kMinInt64, kMaxInt64, 'Int64'); _ws.ensureEightBytes(); _ws.data.setInt64(_ws.offset, value, endian); @@ -317,6 +331,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeFloat32(3.14); // Pi approximation /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeFloat32(double value, [Endian endian = .big]) { _ws.ensureFourBytes(); _ws.data.setFloat32(_ws.offset, value, endian); @@ -332,6 +347,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeFloat64(3.14159265359); // High-precision pi /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeFloat64(double value, [Endian endian = .big]) { _ws.ensureEightBytes(); _ws.data.setFloat64(_ws.offset, value, endian); @@ -351,6 +367,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeBytes(data, 1, 3); // Write [2, 3, 4] /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeBytes(List bytes, [int offset = 0, int? length]) { if (offset < 0) { throw RangeError.value(offset, 'offset', 'Offset must be non-negative'); @@ -364,6 +381,7 @@ extension type BinaryWriter._(_WriterState _ws) { if (len < 0) { throw RangeError.value(len, 'length', 'Length must be non-negative'); } + if (offset + len > bytes.length) { throw RangeError( 'Offset + length exceeds list length: ' @@ -398,6 +416,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeBytes(bytes); /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeVarBytes(List bytes) { writeVarUint(bytes.length); writeBytes(bytes); @@ -430,6 +449,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// /// **Performance:** Highly optimized for ASCII-heavy strings. @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeString(String value, {bool allowMalformed = true}) { final len = value.length; if (len == 0) { @@ -560,6 +580,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// writer.writeString(text); /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void writeVarString(String value, {bool allowMalformed = true}) { final utf8Length = getUtf8Length(value); writeVarUint(utf8Length); @@ -577,6 +598,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// ``` /// @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') // Disable lint to allow positional boolean parameter for simplicity // ignore: avoid_positional_boolean_parameters void writeBool(bool value) { @@ -601,9 +623,11 @@ extension type BinaryWriter._(_WriterState _ws) { /// final packet2 = writer.takeBytes(); /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') Uint8List takeBytes() { final result = Uint8List.sublistView(_ws.list, 0, _ws.offset); _ws._initializeBuffer(); + return result; } @@ -623,6 +647,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// final final = writer.takeBytes(); // Get all data /// ``` @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') Uint8List toBytes() => Uint8List.sublistView(_ws.list, 0, _ws.offset); /// Resets the writer to its initial state, discarding all written data. @@ -635,6 +660,7 @@ extension type BinaryWriter._(_WriterState _ws) { /// Otherwise, writes the Unicode replacement character U+FFFD (�) /// encoded as UTF-8: 0xEF 0xBF 0xBD @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') int _handleMalformed(String v, int i, int offset, bool allow) { if (!allow) { throw FormatException('Invalid UTF-16: lone surrogate at index $i', v, i); @@ -700,6 +726,7 @@ final class _WriterState { var _isInPool = false; @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void _initializeBuffer() { final alignedSize = (_size + 63) & ~63; list = Uint8List(alignedSize); @@ -719,6 +746,7 @@ final class _WriterState { } @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void ensureOneByte() { if (offset + 1 <= capacity) { return; @@ -728,6 +756,7 @@ final class _WriterState { } @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void ensureTwoBytes() { if (offset + 2 <= capacity) { return; @@ -737,6 +766,7 @@ final class _WriterState { } @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void ensureFourBytes() { if (offset + 4 <= capacity) { return; @@ -746,6 +776,7 @@ final class _WriterState { } @pragma('vm:prefer-inline') + @pragma('dart2js:tryInline') void ensureEightBytes() { if (offset + 8 <= capacity) { return; @@ -962,13 +993,13 @@ abstract final class BinaryWriterPool { /// ``` /// /// Returns: A [BinaryWriter] ready for use. - static BinaryWriter acquire() { + static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { if (_pool.isNotEmpty) { final state = _pool.removeLast().._isInPool = false; return BinaryWriter._(state); } - return BinaryWriter(initialBufferSize: _defaultBufferSize); + return BinaryWriter(initialBufferSize: defaultBufferSize); } /// Returns a [BinaryWriter] to the pool for future reuse. diff --git a/lib/src/constants_native.dart b/lib/src/constants_native.dart new file mode 100644 index 0000000..13984d3 --- /dev/null +++ b/lib/src/constants_native.dart @@ -0,0 +1,2 @@ +const kMaxInt64 = 9223372036854775807; +const kMinInt64 = -9223372036854775808; diff --git a/lib/src/constants_web.dart b/lib/src/constants_web.dart new file mode 100644 index 0000000..b1f9266 --- /dev/null +++ b/lib/src/constants_web.dart @@ -0,0 +1,2 @@ +const kMaxInt64 = 9007199254740991; // Max Safe Integer for JavaScript +const kMinInt64 = -9007199254740991; From 172412dd85cf3df0560f27c6754d2a94b7380054 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 8 Jan 2026 10:45:49 +0200 Subject: [PATCH 28/29] Enhance BinaryWriterPool: Add performance counters and tests for pool statistics --- README.md | 121 +++++++++++++------ lib/src/binary_writer.dart | 54 ++++++++- test/unit/binary_writer_test.dart | 189 ++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 91d696f..c019d46 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,13 @@ import 'package:pro_binary/pro_binary.dart'; // Writing data final writer = BinaryWriter(); writer.writeUint32(42); -writer.writeString('Hello, World!'); +writer.writeVarString('Hello, World!'); // Length-prefixed final bytes = writer.takeBytes(); // Reading data final reader = BinaryReader(bytes); -final number = reader.readUint32(); // 42 -final text = reader.readString(13); // 'Hello, World!' +final number = reader.readUint32(); // 42 +final text = reader.readVarString(); // 'Hello, World!' ``` ## Core API @@ -60,9 +60,16 @@ writer.writeFloat64(3.14159265359); writer.writeVarUint(42); // Unsigned VarInt writer.writeVarInt(-42); // Signed VarInt with ZigZag +// Binary data +writer.writeBytes([1, 2, 3]); // Raw bytes +writer.writeVarBytes([1, 2, 3]); // Length-prefixed bytes + // Strings -writer.writeString('text'); // Fixed UTF-8 string (you control length) -writer.writeVarString('Hello'); // Length-prefixed UTF-8 string (auto length) +writer.writeString('text'); // Raw UTF-8 string (no length prefix) +writer.writeVarString('Hello'); // Length-prefixed UTF-8 string + +// Boolean +writer.writeBool(true); // Single byte (0x01 or 0x00) // Get result final bytes = writer.takeBytes(); // Gets bytes and resets @@ -84,18 +91,32 @@ final f64 = reader.readFloat64(); final count = reader.readVarUint(); final delta = reader.readVarInt(); +// Binary data +final data = reader.readBytes(10); // Read 10 bytes +final varData = reader.readVarBytes(); // Read length-prefixed bytes +final remaining = reader.readRemainingBytes(); // Read all remaining + // Strings -final text = reader.readString(10); // Read 10 UTF-8 bytes (you specify length) -final message = reader.readVarString(); // Read length-prefixed string (auto length) +final text = reader.readString(10); // Read 10 UTF-8 bytes +final message = reader.readVarString(); // Read length-prefixed string + +// Boolean +final flag = reader.readBool(); // Read boolean (0x00 = false, other = true) // Navigation reader.skip(4); // Skip bytes +reader.seek(10); // Jump to position 10 +reader.rewind(2); // Move back 2 bytes final peek = reader.peekBytes(2); // Look ahead without consuming reader.reset(); // Go back to start // Check state print(reader.offset); // Current position +print(reader.length); // Total buffer size print(reader.availableBytes); // Bytes left to read +if (reader.hasBytes(4)) { // Check if enough bytes available + final value = reader.readUint32(); +} ``` ## Real-World Examples @@ -106,33 +127,33 @@ print(reader.availableBytes); // Bytes left to read // Encode message final writer = BinaryWriter(); writer.writeUint8(0x42); // Message type -writer.writeVarUint(payload.length); -writer.writeBytes(payload); +writer.writeVarBytes(payload); // Length-prefixed payload sendToServer(writer.takeBytes()); // Decode message final reader = BinaryReader(received); final type = reader.readUint8(); -final length = reader.readVarUint(); -final payload = reader.readBytes(length); +final payload = reader.readVarBytes(); // Reads length + data ``` ### Length-Prefixed Strings ```dart -// Write +// Recommended: Use writeVarString (automatic length) +writer.writeVarString('Hello, 世界! 🌍'); + +// Or manually (equivalent to above): final text = 'Hello, 世界! 🌍'; -final encoded = utf8.encode(text); -writer.writeVarUint(encoded.length); +final utf8Length = getUtf8Length(text); // Calculate UTF-8 byte length +writer.writeVarUint(utf8Length); writer.writeString(text); -// or simply -writer.writeVarString(text); -// Read +// Reading: Use readVarString (reads length + data) +final text = reader.readVarString(); + +// Or manually (equivalent to above): final length = reader.readVarUint(); final text = reader.readString(length); -// or simply -final text = reader.readVarString(); ``` ### Struct-like Data @@ -143,19 +164,18 @@ class Player { final String name; final double x, y; + Player(this.id, this.name, this.x, this.y); + void writeTo(BinaryWriter w) { w.writeUint32(id); - final nameBytes = utf8.encode(name); - w.writeVarUint(nameBytes.length); - w.writeString(name); + w.writeVarString(name); // Length-prefixed string w.writeFloat64(x); w.writeFloat64(y); } static Player readFrom(BinaryReader r) { final id = r.readUint32(); - final nameLen = r.readVarUint(); - final name = r.readString(nameLen); + final name = r.readVarString(); // Reads length + string final x = r.readFloat64(); final y = r.readFloat64(); return Player(id, name, x, y); @@ -174,7 +194,8 @@ final writer = BinaryWriter(initialBufferSize: 128); // Default: 128 bytes **Buffer Management:** - Initial capacity: 128 bytes (configurable) -- Growth strategy: `newCapacity = max(currentCapacity * 1.5, currentCapacity + requiredBytes)` with 64-byte alignment +- Growth strategy: `newCapacity = ((currentCapacity * 1.5).ceil() + 63) & ~63` (1.5× + 64-byte alignment) +- Minimum expansion: Ensures space for requested bytes - Resets buffer without reallocation: `writer.reset()` - Takes ownership of buffer: `writer.takeBytes()` (one-time use, resets writer) - Creates view without reset: `writer.toBytes()` (reusable) @@ -188,6 +209,10 @@ final writer = BinaryWriter(initialBufferSize: 128); // Default: 128 bytes - Strings: `writeString` (raw UTF-8), `writeVarString` (length-prefixed) - Boolean: `writeBool` (1 byte: 0x00 or 0x01) +**Helper Functions:** + +- `getUtf8Length(String)`: Calculate UTF-8 byte length without encoding + ### BinaryReader ```dart @@ -283,20 +308,29 @@ final writer = BinaryWriter(initialBufferSize: 8192); // For bulk writes **Use object pooling** for high-frequency operations: ```dart -// Acquire from pool +// Acquire from pool (default 1KB buffer) final writer = BinaryWriterPool.acquire(); try { writer.writeUint32(value); - final bytes = writer.takeBytes(); + final bytes = writer.toBytes(); // Get bytes, keep writer alive send(bytes); } finally { - // Return to pool for reuse + // Return to pool for reuse (max 32 writers, max 64KB buffers) BinaryWriterPool.release(writer); } // Pool statistics -print(BinaryWriterPool.poolSize); // Available writers -print(BinaryWriterPool.peakPoolSize); // High water mark +final stats = BinaryWriterPool.stats; +print('Pooled writers: ${stats.pooled}'); // Current pool size +print('Max pool size: ${stats.maxPoolSize}'); // Maximum capacity (32) +print('Peak pool size: ${stats.peakPoolSize}'); // High water mark +print('Acquire hits: ${stats.acquireHit}'); // Successful reuses +print('Acquire misses: ${stats.acquireMiss}'); // New allocations +print('Hit rate: ${(stats.hitRate * 100).toStringAsFixed(1)}%'); // Cache efficiency +print('Discarded: ${stats.discardedLargeBuffers}'); // Oversized buffers + +// Clear pool manually +BinaryWriterPool.clear(); ``` **Choose correct integer type**: @@ -349,13 +383,17 @@ For **fixed-length** strings, calculate UTF-8 byte length: ```dart final text = 'Hello, 世界!'; -final bytes = utf8.encode(text); -writer.writeUint16(bytes.length); // Store byte length +final utf8Length = getUtf8Length(text); // Calculate without encoding +writer.writeUint16(utf8Length); // Store byte length writer.writeString(text); // Reading final byteLength = reader.readUint16(); final text = reader.readString(byteLength); + +// Handle malformed UTF-8 sequences +final strict = reader.readString(10, allowMalformed: false); // Throws on error +final lenient = reader.readString(10); // allowMalformed: true (default) - uses � ``` ### Error Handling @@ -399,10 +437,25 @@ void writeMessage(BinaryWriter w, MessageType type, dynamic payload) { // No payload break; case MessageType.data: - w.writeVarBytes(payload as List); + w.writeVarBytes(payload as List); // Length-prefixed + break; + case MessageType.ack: + w.writeUint32(payload as int); // Sequence number + break; + } +} + +void readMessage(BinaryReader r) { + final type = MessageType.values[r.readUint8()]; + switch (type) { + case MessageType.ping: + // No payload + break; + case MessageType.data: + final payload = r.readVarBytes(); // Reads length + data break; case MessageType.ack: - w.writeUint32(payload as int); // Sequence number + final seqNum = r.readUint32(); break; } } diff --git a/lib/src/binary_writer.dart b/lib/src/binary_writer.dart index 91005a8..fd62bcc 100644 --- a/lib/src/binary_writer.dart +++ b/lib/src/binary_writer.dart @@ -966,6 +966,12 @@ abstract final class BinaryWriterPool { /// 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 @@ -995,10 +1001,12 @@ abstract final class BinaryWriterPool { /// Returns: A [BinaryWriter] ready for use. static BinaryWriter acquire([int defaultBufferSize = _defaultBufferSize]) { if (_pool.isNotEmpty) { + _acquireHit++; final state = _pool.removeLast().._isInPool = false; return BinaryWriter._(state); } + _acquireMiss++; return BinaryWriter(initialBufferSize: defaultBufferSize); } @@ -1040,6 +1048,13 @@ abstract final class BinaryWriterPool { ..offset = 0 .._isInPool = true; _pool.add(state); + + // Track peak pool size + if (_pool.length > _peakPoolSize) { + _peakPoolSize = _pool.length; + } + } else if (state.capacity > _maxReusableCapacity) { + _discardedLargeBuffers++; } } @@ -1052,17 +1067,26 @@ abstract final class BinaryWriterPool { /// - `'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.getStatistics(); - /// print('Pooled writers: ${stats['pooled']}'); // 5 + /// 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. @@ -1078,7 +1102,13 @@ abstract final class BinaryWriterPool { /// ```dart /// BinaryWriterPool.clear(); // All pooled writers discarded /// ``` - static void clear() => _pool.clear(); + static void clear() { + _pool.clear(); + _acquireHit = 0; + _acquireMiss = 0; + _peakPoolSize = 0; + _discardedLargeBuffers = 0; + } } extension type PoolStatistics(Map _stats) { @@ -1093,4 +1123,22 @@ extension type PoolStatistics(Map _stats) { /// 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/test/unit/binary_writer_test.dart b/test/unit/binary_writer_test.dart index 95586d8..07da45f 100644 --- a/test/unit/binary_writer_test.dart +++ b/test/unit/binary_writer_test.dart @@ -2147,6 +2147,195 @@ void main() { expect(stats.maxReusableCapacity, equals(64 * 1024)); }); + test('acquireHit increments on pool reuse', () { + expect(BinaryWriterPool.stats.acquireHit, equals(0)); + + // First acquire is a miss + final writer1 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.acquireHit, equals(0)); + expect(BinaryWriterPool.stats.acquireMiss, equals(1)); + + BinaryWriterPool.release(writer1); + + // Second acquire is a hit (reuses pooled writer) + final writer2 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.acquireHit, equals(1)); + expect(BinaryWriterPool.stats.acquireMiss, equals(1)); + + BinaryWriterPool.release(writer2); + }); + + test('acquireMiss increments on new allocation', () { + expect(BinaryWriterPool.stats.acquireMiss, equals(0)); + + // Pool is empty, should create new writers + final writer1 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.acquireMiss, equals(1)); + + final writer2 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.acquireMiss, equals(2)); + + final writer3 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.acquireMiss, equals(3)); + + BinaryWriterPool.release(writer1); + BinaryWriterPool.release(writer2); + BinaryWriterPool.release(writer3); + + // Now pool has 3 writers, no new allocations needed + final writer4 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.acquireMiss, equals(3)); + expect(BinaryWriterPool.stats.acquireHit, equals(1)); + + BinaryWriterPool.release(writer4); + }); + + test('peakPoolSize tracks maximum pool size', () { + expect(BinaryWriterPool.stats.peakPoolSize, equals(0)); + + // Create 3 writers simultaneously (all will be misses) + final writer1 = BinaryWriterPool.acquire(); + final writer2 = BinaryWriterPool.acquire(); + final writer3 = BinaryWriterPool.acquire(); + + // Release all 3 - pool size will grow to 3 + BinaryWriterPool.release(writer1); + expect(BinaryWriterPool.stats.pooled, equals(1)); + expect(BinaryWriterPool.stats.peakPoolSize, equals(1)); + + BinaryWriterPool.release(writer2); + expect(BinaryWriterPool.stats.pooled, equals(2)); + expect(BinaryWriterPool.stats.peakPoolSize, equals(2)); + + BinaryWriterPool.release(writer3); + expect(BinaryWriterPool.stats.pooled, equals(3)); + expect(BinaryWriterPool.stats.peakPoolSize, equals(3)); + + // Acquire one (pool size decreases but peak stays) + final writer4 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.pooled, equals(2)); + expect(BinaryWriterPool.stats.peakPoolSize, equals(3)); + + BinaryWriterPool.release(writer4); + }); + + test('discardedLargeBuffers increments when buffer exceeds limit', () { + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(0)); + + final writer = BinaryWriterPool.acquire(); + + // Write enough data to expand buffer beyond 64 KiB + final largeData = List.filled(70 * 1024, 42); + writer.writeBytes(largeData); + + BinaryWriterPool.release(writer); + + // Buffer should be discarded + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(1)); + expect(BinaryWriterPool.stats.pooled, equals(0)); + + // Create another large buffer + final writer2 = BinaryWriterPool.acquire() + ..writeBytes(List.filled(100 * 1024, 1)); + BinaryWriterPool.release(writer2); + + expect(BinaryWriterPool.stats.discardedLargeBuffers, equals(2)); + }); + + test('totalAcquires returns sum of hits and misses', () { + expect(BinaryWriterPool.stats.totalAcquires, equals(0)); + + final writer1 = BinaryWriterPool.acquire(); // miss + expect(BinaryWriterPool.stats.totalAcquires, equals(1)); + + BinaryWriterPool.release(writer1); + + final writer2 = BinaryWriterPool.acquire(); // hit + expect(BinaryWriterPool.stats.totalAcquires, equals(2)); + + final writer3 = BinaryWriterPool.acquire(); // miss + expect(BinaryWriterPool.stats.totalAcquires, equals(3)); + + expect(BinaryWriterPool.stats.acquireHit, equals(1)); + expect(BinaryWriterPool.stats.acquireMiss, equals(2)); + + BinaryWriterPool.release(writer2); + BinaryWriterPool.release(writer3); + }); + + test('hitRate returns correct percentage', () { + // Initially no acquires + expect(BinaryWriterPool.stats.hitRate, equals(0.0)); + + // First acquire is always a miss + final writer1 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.hitRate, equals(0.0)); // 0/1 = 0% + + BinaryWriterPool.release(writer1); + + // Second acquire is a hit + final writer2 = BinaryWriterPool.acquire(); + expect(BinaryWriterPool.stats.hitRate, equals(0.5)); // 1/2 = 50% + + BinaryWriterPool.release(writer2); + + // Third acquire is a hit + final writer3 = BinaryWriterPool.acquire(); + expect( + BinaryWriterPool.stats.hitRate, + closeTo(0.666, 0.001), + ); // 2/3 ≈ 66.7% + + BinaryWriterPool.release(writer3); + }); + + test('clear resets all statistics', () { + // Generate some activity + // Create 2 writers to have one in pool after operations + final writerA = BinaryWriterPool.acquire(); // miss + final writerB = BinaryWriterPool.acquire(); // miss + writerA.writeUint32(1); + BinaryWriterPool.release(writerA); // pool=1 + + // Reuse writerA + final writer2 = + BinaryWriterPool.acquire() // hit, pool=0 + ..writeUint32(2); + BinaryWriterPool.release(writer2); // pool=1 + + // Reuse again + final writer3 = + BinaryWriterPool.acquire() // hit, pool=0 + ..writeUint32(3); + BinaryWriterPool.release(writer3); // pool=1 + + // Now use writerB with large buffer + writerB.writeBytes(List.filled(70 * 1024, 1)); + BinaryWriterPool.release(writerB); // Discarded, pool stays =1 + + // Verify stats are non-zero + final stats = BinaryWriterPool.stats; + expect(stats.pooled, equals(1)); // writerA is still pooled + expect(stats.acquireHit, equals(2)); // writer2 and writer3 were hits + expect(stats.acquireMiss, equals(2)); // writerA and writerB were misses + expect( + stats.peakPoolSize, + equals(1), + ); // Never more than 1 in pool at once + expect(stats.discardedLargeBuffers, equals(1)); // writerB was discarded + + // Clear should reset everything + BinaryWriterPool.clear(); + + final clearedStats = BinaryWriterPool.stats; + expect(clearedStats.pooled, equals(0)); + expect(clearedStats.acquireHit, equals(0)); + expect(clearedStats.acquireMiss, equals(0)); + expect(clearedStats.peakPoolSize, equals(0)); + expect(clearedStats.discardedLargeBuffers, equals(0)); + expect(clearedStats.hitRate, equals(0.0)); + }); + test('pool respects max pool size', () { // Create and release more writers than the pool can hold final writers = []; From 36491c3a37a6c7c3b53516f8958a070a449dee6a Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Thu, 8 Jan 2026 10:54:09 +0200 Subject: [PATCH 29/29] Enhance BinaryWriter and BinaryReader: Add new methods for variable-length encoding, improve documentation, and expand test coverage --- CHANGELOG.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09e70d2..dbaf15e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,30 @@ **Improvements:** +- **feat**: New methods and properties + - `BinaryWriterPool` for reusing `BinaryWriter` instances + - `getUtf8Length(String)` to calculate UTF-8 byte length without encoding + - `writeVarString(String)` and `readVarString()` for variable-length string encoding + - `writeBool` and `readBool` methods for boolean values + - `writeVarUint` and `readVarUint` for variable-length unsigned integers + - `writeVarInt` and `readVarInt` for variable-length signed integers + - `writeVarBytes` and `readVarBytes` for variable-length byte arrays + - Navigation methods in `BinaryReader`: `peekBytes()`, `skip()`, `seek()`, `rewind()`, and `reset()` - **docs**: Comprehensive documentation overhaul - Added detailed API documentation with usage examples for all methods - Documented `writeVarString()`, `readVarString()`, and `getUtf8Length()` - Included performance notes and best practices - Added inline comments explaining complex encoding algorithms - - Improved README with real-world examples and migration guide -- **test**: Added 23 new comprehensive tests - - 7 tests for `writeVarString()` (ASCII, UTF-8, emoji, empty, mixed, round-trip, malformed) - - 8 tests for `getUtf8Length()` (ASCII, empty, 2-byte, 3-byte, 4-byte, mixed, validation, surrogates) - - 8 tests for `readVarString()` (basic, UTF-8, emoji, empty, multiple, error handling) +- **test**: Expanded test suite + - Coverage for all new methods and edge cases + - Performance benchmarks for encoding/decoding functions + - Validation tests for UTF-8 handling and error scenarios +- **improvement**: Refactored internal codebase + - Improved modularity and readability + - Enhanced error handling with descriptive messages + - Optimized buffer management for better performance + +- **fix**: Resolved known issues ## 2.2.0