From b9459fe55f4ac2573ca665d7a72dea00a2a43f78 Mon Sep 17 00:00:00 2001 From: Andrii Ivanov Date: Mon, 15 Dec 2025 14:34:13 +0200 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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++) {