|
1 | 1 | // Copyright (c) 2026 Alexander Salas Bastidas <ajsb85@firechip.dev> |
2 | 2 | // SPDX-License-Identifier: MIT |
| 3 | +// |
| 4 | +// ignore_for_file: avoid_print |
3 | 5 |
|
4 | 6 | /// Throughput micro-benchmarks for the COBS / COBS/R codecs. |
5 | 7 | /// |
6 | 8 | /// Dev tooling only: this file is excluded from the published package via |
7 | | -/// `.pubignore` and is not part of the public API. |
| 9 | +/// `.pubignore`, is not part of the public API, and has no package |
| 10 | +/// dependencies (a plain `Stopwatch`, so it runs on every supported SDK). |
8 | 11 | /// |
9 | 12 | /// Run with: |
10 | 13 | /// |
11 | 14 | /// ```sh |
12 | 15 | /// dart run benchmark/cobs_benchmark.dart |
13 | 16 | /// ``` |
14 | 17 | /// |
15 | | -/// Each benchmark processes a single 1 KiB (1024-byte) payload per `run()`. |
16 | | -/// `exercise()` is overridden to invoke `run()` exactly once, so |
17 | | -/// `BenchmarkBase.measure()` returns microseconds per operation. Throughput is |
18 | | -/// reported as `payloadBytes / time`; because one byte per microsecond equals |
19 | | -/// one decimal megabyte per second, `MB/s == payloadBytes / usPerOp`. |
| 18 | +/// Each operation processes a single 1 KiB (1024-byte) payload; throughput is |
| 19 | +/// reported as decimal MB/s (`payloadBytes * iterations / seconds / 1e6`). |
20 | 20 | library; |
21 | 21 |
|
22 | | -import 'dart:io'; |
23 | 22 | import 'dart:typed_data'; |
24 | 23 |
|
25 | | -import 'package:benchmark_harness/benchmark_harness.dart'; |
26 | 24 | import 'package:cobs_codec/cobs_codec.dart'; |
27 | 25 |
|
28 | | -/// Size of the representative payload, in bytes (1 KiB). |
29 | | -const int payloadBytes = 1024; |
30 | | - |
31 | | -/// Builds a deterministic, mostly-non-zero payload with roughly one in eight |
32 | | -/// bytes set to `0x00`, so the COBS zero-delimited block path is exercised. |
33 | | -/// |
34 | | -/// Uses a fixed-seed xorshift32 generator so the payload is identical across |
35 | | -/// runs and platforms, making results comparable over time. |
36 | | -Uint8List buildPayload(int length) { |
37 | | - final data = Uint8List(length); |
38 | | - var state = 0x12345678; // fixed seed -> reproducible pattern |
| 26 | +/// A deterministic 1 KiB payload with roughly one zero byte in eight, so the |
| 27 | +/// COBS block-splitting path is exercised. |
| 28 | +Uint8List _makePayload(int length) { |
| 29 | + final bytes = Uint8List(length); |
| 30 | + var state = 0x12345678; |
39 | 31 | for (var i = 0; i < length; i++) { |
40 | | - state ^= (state << 13) & 0xFFFFFFFF; |
41 | | - state ^= state >> 17; |
42 | | - state ^= (state << 5) & 0xFFFFFFFF; |
43 | | - final b = state & 0xFF; |
44 | | - // ~1/8 of bytes become zero; the rest keep a guaranteed-non-zero value. |
45 | | - data[i] = (b & 7) == 0 ? 0 : b; |
| 32 | + state = (state * 1103515245 + 12345) & 0x7fffffff; |
| 33 | + bytes[i] = (state % 8 == 0) ? 0 : (state & 0xff); |
46 | 34 | } |
47 | | - return data; |
| 35 | + return bytes; |
48 | 36 | } |
49 | 37 |
|
50 | | -/// Benchmarks a full COBS encode of the 1 KiB payload. |
51 | | -class CobsEncodeBenchmark extends BenchmarkBase { |
52 | | - CobsEncodeBenchmark(this.payload) : super('cobsEncode'); |
53 | | - |
54 | | - final Uint8List payload; |
55 | | - |
56 | | - @override |
57 | | - void exercise() => run(); |
58 | | - |
59 | | - @override |
60 | | - void run() { |
61 | | - cobsEncode(payload); |
| 38 | +/// Warms up, then times [operation] over many iterations and prints its |
| 39 | +/// throughput in decimal MB/s relative to [payloadBytes] bytes per call. |
| 40 | +double _benchmark(String name, int payloadBytes, void Function() operation) { |
| 41 | + for (var i = 0; i < 20000; i++) { |
| 42 | + operation(); |
62 | 43 | } |
63 | | -} |
64 | | - |
65 | | -/// Benchmarks a full COBS decode of the encoded 1 KiB payload. |
66 | | -class CobsDecodeBenchmark extends BenchmarkBase { |
67 | | - CobsDecodeBenchmark(this.encoded) : super('cobsDecode'); |
68 | | - |
69 | | - final Uint8List encoded; |
70 | | - |
71 | | - @override |
72 | | - void exercise() => run(); |
73 | | - |
74 | | - @override |
75 | | - void run() { |
76 | | - cobsDecode(encoded); |
77 | | - } |
78 | | -} |
79 | | - |
80 | | -/// Benchmarks a full COBS/R encode of the 1 KiB payload. |
81 | | -class CobsrEncodeBenchmark extends BenchmarkBase { |
82 | | - CobsrEncodeBenchmark(this.payload) : super('cobsrEncode'); |
83 | | - |
84 | | - final Uint8List payload; |
85 | | - |
86 | | - @override |
87 | | - void exercise() => run(); |
88 | | - |
89 | | - @override |
90 | | - void run() { |
91 | | - cobsrEncode(payload); |
| 44 | + const iterations = 500000; |
| 45 | + final stopwatch = Stopwatch()..start(); |
| 46 | + for (var i = 0; i < iterations; i++) { |
| 47 | + operation(); |
92 | 48 | } |
| 49 | + stopwatch.stop(); |
| 50 | + final seconds = stopwatch.elapsedMicroseconds / 1e6; |
| 51 | + final mbps = payloadBytes * iterations / seconds / 1e6; |
| 52 | + print('${name.padRight(12)} ${mbps.toStringAsFixed(1)} MB/s'); |
| 53 | + return mbps; |
93 | 54 | } |
94 | 55 |
|
95 | 56 | void main() { |
96 | | - final payload = buildPayload(payloadBytes); |
| 57 | + final payload = _makePayload(1024); |
97 | 58 | final encoded = cobsEncode(payload); |
98 | | - final zeros = payload.where((b) => b == 0).length; |
99 | | - |
100 | | - stdout.writeln('cobs_codec throughput benchmark'); |
101 | | - stdout.writeln(' Dart: ${Platform.version}'); |
102 | | - stdout.writeln( |
103 | | - ' OS: ${Platform.operatingSystem} ' |
104 | | - '(${Platform.operatingSystemVersion})', |
105 | | - ); |
106 | | - stdout.writeln( |
107 | | - ' Payload: $payloadBytes bytes ' |
108 | | - '($zeros zero / ${payloadBytes - zeros} non-zero), ' |
109 | | - 'COBS-encoded to ${encoded.length} bytes', |
110 | | - ); |
111 | | - stdout.writeln(); |
112 | 59 |
|
113 | | - final benchmarks = <BenchmarkBase>[ |
114 | | - CobsEncodeBenchmark(payload), |
115 | | - CobsDecodeBenchmark(encoded), |
116 | | - CobsrEncodeBenchmark(payload), |
117 | | - ]; |
118 | | - |
119 | | - stdout.writeln(' benchmark us/op MB/s'); |
120 | | - stdout.writeln(' ----------- -------- ---------'); |
121 | | - for (final benchmark in benchmarks) { |
122 | | - final usPerOp = benchmark.measure(); // microseconds per single run() |
123 | | - // 1 byte/us == 1e6 bytes/s == 1 MB/s (decimal), so MB/s == bytes / us. |
124 | | - final mbPerSecond = payloadBytes / usPerOp; |
125 | | - stdout.writeln( |
126 | | - ' ${benchmark.name.padRight(11)} ' |
127 | | - '${usPerOp.toStringAsFixed(3).padLeft(8)} ' |
128 | | - '${mbPerSecond.toStringAsFixed(1).padLeft(9)}', |
129 | | - ); |
130 | | - } |
| 60 | + _benchmark('cobsEncode', payload.length, () => cobsEncode(payload)); |
| 61 | + _benchmark('cobsDecode', payload.length, () => cobsDecode(encoded)); |
| 62 | + _benchmark('cobsrEncode', payload.length, () => cobsrEncode(payload)); |
131 | 63 | } |
0 commit comments