Add benchmark

This commit is contained in:
2026-05-04 14:40:11 -07:00
parent b03ec9eba9
commit 4a6a09cff1
18 changed files with 3922 additions and 12 deletions
Generated
+445
View File
@@ -11,6 +11,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "1.0.0" version = "1.0.0"
@@ -61,6 +67,57 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.1" version = "4.6.1"
@@ -107,6 +164,79 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]] [[package]]
name = "env_filter" name = "env_filter"
version = "1.0.1" version = "1.0.1"
@@ -130,18 +260,85 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.24" version = "0.2.24"
@@ -166,6 +363,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "js-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.29"
@@ -178,12 +393,67 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "once_cell_polyfill" name = "once_cell_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.1" version = "1.13.1"
@@ -217,6 +487,26 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.3" version = "1.12.3"
@@ -251,10 +541,36 @@ name = "roto"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"criterion",
"env_logger", "env_logger",
"log", "log",
] ]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@@ -275,6 +591,25 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_json"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -292,6 +627,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.24" version = "1.0.24"
@@ -304,6 +649,80 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
@@ -318,3 +737,29 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "zerocopy"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+7
View File
@@ -7,3 +7,10 @@ edition = "2024"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "hackers_bench"
harness = false
+83
View File
@@ -18,6 +18,14 @@ returning a slice of the bytes written.
`CodeGeneratorResponse` to stdout. `protoc` then writes those `.rs` files to disk. The generated `CodeGeneratorResponse` to stdout. `protoc` then writes those `.rs` files to disk. The generated
files are included directly in the crate that uses the protobuffers. files are included directly in the crate that uses the protobuffers.
Sample usage:
```
protoc -Iproto/ proto/hackers.proto --plugin=./target/debug/protoc-gen-roto --roto_out=src/
```
This will generate a file, src/hackers.rs.
## Generated code ## Generated code
For each protobuf message roto generates two types: For each protobuf message roto generates two types:
@@ -117,6 +125,81 @@ using `FieldIterator` and records the byte offset of each field's tag. Subsequen
call `ProtoAccessor::get_value_at(offset)` — no re-scanning. For repeated fields, the start and call `ProtoAccessor::get_value_at(offset)` — no re-scanning. For repeated fields, the start and
end offsets of the field range are recorded to bound iteration efficiently. end offsets of the field range are recorded to bound iteration efficiently.
## Benchmarks
Two benchmark suites share the same binary data files and the same four
measurement groups:
| Group | What is timed |
| --------------- | ------------------------------------------------------- |
| `shallow_parse` | Become ready to read any field (one scan / full decode) |
| `deep_parse` | Walk the full tree: Campaign → Operations → Hackers |
| `field_access` | Read individual fields on an already-parsed message |
| `iterate` | Count top-level and nested repeated fields |
### 1 — Generate the shared data files (do this once)
Data files are written to `data/bench/`.
```sh
cargo run --release --bin gen_bench_data -- --preset tiny
cargo run --release --bin gen_bench_data -- --preset small
cargo run --release --bin gen_bench_data -- --preset medium
cargo run --release --bin gen_bench_data -- --preset large
```
For even larger inputs use `--preset huge` (~500 MB) or set the knobs
directly:
```sh
# ~50 MB: 500 operations × 100 KB stolen_data each
cargo run --release --bin gen_bench_data -- --ops 500 --stolen-kb 100 --output data/bench/50mb.pb
```
### 2 — Rust benchmark (criterion)
```sh
cargo bench --bench hackers_bench
```
HTML reports are written to `target/criterion/`. Run a single group:
```sh
cargo bench --bench hackers_bench -- shallow_parse
```
### 3 — C / upb benchmark
Requires protobuf ≥ 21 with `protoc-gen-upb` (ships with modern `protoc`).
```sh
cd upb_test
make # compiles hackers_bench from the pre-generated upb files
./hackers_bench
```
To regenerate the upb C files from `proto/hackers.proto`:
```sh
cd upb_test && make regen
```
### Interpreting the comparison
The two libraries have fundamentally different models:
- **roto `shallow_parse`** does one linear scan recording byte offsets — no
allocation, no field decoding. Subsequent field reads decode on demand at
the stored offset.
- **upb `Campaign_parse`** fully decodes the entire message tree into
arena-allocated structs upfront. Subsequent field reads are direct struct
member lookups (~1 ns).
The result: roto's parse is faster and allocation-free; upb's field access
after parsing is faster. For workloads that read every field the costs
invert; for workloads that read a handful of fields from large messages roto
wins.
## Literature ## Literature
https://protobuf.dev/programming-guides/encoding/ https://protobuf.dev/programming-guides/encoding/
+215
View File
@@ -0,0 +1,215 @@
//! Benchmark suite for roto — themed after the 1995 film *Hackers*.
//!
//! Proto schema: `proto/hackers.proto`
//! Generated types: `src/hackers.rs` (via `protoc-gen-roto`)
//!
//! # Setup
//!
//! Generate the data files once before running benchmarks:
//!
//! ```sh
//! cargo run --release --bin gen_bench_data -- --preset tiny
//! cargo run --release --bin gen_bench_data -- --preset small
//! cargo run --release --bin gen_bench_data -- --preset medium
//! cargo run --release --bin gen_bench_data -- --preset large
//! ```
//!
//! Then run:
//!
//! ```sh
//! cargo bench --bench hackers_bench
//! ```
//!
//! Benchmark groups:
//! - `shallow_parse` — `Campaign::new(data)`, one scan of the whole blob
//! - `deep_parse` — Campaign → Operations → Hackers (a `::new()` per level)
//! - `field_access` — individual field reads on pre-parsed messages (O(1))
//! - `iterate` — counting repeated fields at different nesting depths
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use roto::hackers::{Campaign, Hacker, Operation, Worm};
use std::hint::black_box;
// =============================================================================
// Data loading
// =============================================================================
/// Load a pre-generated data file from `data/bench/<name>.pb`.
/// Returns `None` (and prints a hint) if the file does not exist.
fn load(name: &str) -> Option<Vec<u8>> {
let path = format!("data/bench/{name}.pb");
match std::fs::read(&path) {
Ok(data) => Some(data),
Err(_) => {
eprintln!(
"[skip] {path} not found — \
run `cargo run --release --bin gen_bench_data -- --preset {name}` first"
);
None
}
}
}
// =============================================================================
// Benchmarks
// =============================================================================
/// `Campaign::new()` — one linear scan to record field offsets, no allocation.
/// Throughput reported in MB/s so different sizes are directly comparable.
fn bench_shallow_parse(c: &mut Criterion) {
let cases = [
("tiny", load("tiny")),
("small", load("small")),
("medium", load("medium")),
("large", load("large")),
];
let mut group = c.benchmark_group("shallow_parse");
for (label, maybe_data) in &cases {
let Some(data) = maybe_data else { continue };
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_with_input(BenchmarkId::new("Campaign::new", label), data, |b, data| {
b.iter(|| Campaign::new(black_box(data)).unwrap())
});
}
group.finish();
}
/// Walk every level of the tree: Campaign → Operations → Hackers.
/// Each `::new()` is an additional linear scan of that sub-message's bytes.
fn bench_deep_parse(c: &mut Criterion) {
let cases = [
("tiny", load("tiny")),
("small", load("small")),
("medium", load("medium")),
];
let mut group = c.benchmark_group("deep_parse");
for (label, maybe_data) in &cases {
let Some(data) = maybe_data else { continue };
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_with_input(
BenchmarkId::new("Campaign+Ops+Hackers", label),
data,
|b, data| {
b.iter(|| {
let campaign = Campaign::new(data).unwrap();
let mut hacker_count = 0usize;
for op_res in campaign.operations() {
let (op_bytes, _) = op_res.unwrap();
let op = Operation::new(op_bytes).unwrap();
for crew_res in op.crew() {
let (hacker_bytes, _) = crew_res.unwrap();
let hacker = Hacker::new(hacker_bytes).unwrap();
let _ = black_box(hacker.handle().unwrap());
hacker_count += 1;
}
}
black_box(hacker_count)
})
},
);
}
group.finish();
}
/// O(1) field accesses on pre-parsed messages.
/// Measures only the decode step at a known offset — not the scan.
fn bench_field_access(c: &mut Criterion) {
let Some(data) = load("small") else { return };
let campaign = Campaign::new(&data).unwrap();
let (op_bytes, _) = campaign.operations().next().unwrap().unwrap();
let op = Operation::new(op_bytes).unwrap();
let (hacker_bytes, _) = op.crew().next().unwrap().unwrap();
let hacker = Hacker::new(hacker_bytes).unwrap();
let worm = Worm::new(op.worm().unwrap()).unwrap();
let mut group = c.benchmark_group("field_access");
group.bench_function("campaign::name", |b| {
b.iter(|| black_box(campaign.name().unwrap()))
});
group.bench_function("campaign::total_bytes_stolen", |b| {
b.iter(|| black_box(campaign.total_bytes_stolen().unwrap()))
});
group.bench_function("operation::codename", |b| {
b.iter(|| black_box(op.codename().unwrap()))
});
group.bench_function("operation::timestamp", |b| {
b.iter(|| black_box(op.timestamp().unwrap()))
});
group.bench_function("operation::successful", |b| {
b.iter(|| black_box(op.successful().unwrap()))
});
group.bench_function("hacker::handle", |b| {
b.iter(|| black_box(hacker.handle().unwrap()))
});
group.bench_function("hacker::skill_level (f32)", |b| {
b.iter(|| black_box(hacker.skill_level().unwrap()))
});
group.bench_function("hacker::is_elite (bool)", |b| {
b.iter(|| black_box(hacker.is_elite().unwrap()))
});
group.bench_function("worm::polymorphic (bool)", |b| {
b.iter(|| black_box(worm.polymorphic().unwrap()))
});
group.bench_function("worm::payload (bytes)", |b| {
b.iter(|| black_box(worm.payload().unwrap()))
});
group.finish();
}
/// Iterate repeated fields at different depths.
fn bench_iterate(c: &mut Criterion) {
let cases = [
("tiny", load("tiny")),
("small", load("small")),
("medium", load("medium")),
];
let mut group = c.benchmark_group("iterate");
for (label, maybe_data) in &cases {
let Some(data) = maybe_data else { continue };
// Top-level repeated field — walk Operation blobs, no inner parse.
group.bench_with_input(
BenchmarkId::new("count_operations", label),
data,
|b, data| {
b.iter(|| {
let campaign = Campaign::new(data).unwrap();
black_box(campaign.operations().count())
})
},
);
// Nested repeated field — parse each Operation to reach its crew.
group.bench_with_input(
BenchmarkId::new("count_all_crew", label),
data,
|b, data| {
b.iter(|| {
let campaign = Campaign::new(data).unwrap();
let mut n = 0usize;
for op_res in campaign.operations() {
let (op_bytes, _) = op_res.unwrap();
n += Operation::new(op_bytes).unwrap().crew().count();
}
black_box(n)
})
},
);
}
group.finish();
}
criterion_group!(
benches,
bench_shallow_parse,
bench_deep_parse,
bench_field_access,
bench_iterate
);
criterion_main!(benches);
+1
View File
@@ -0,0 +1 @@
bench/
+56
View File
@@ -0,0 +1,56 @@
syntax = "proto3";
message Tool {
string name = 1;
string version = 2;
bytes payload = 3;
bool is_active = 4;
int32 exploit_count = 5;
}
message Connection {
string host = 1;
int32 port = 2;
bool encrypted = 3;
int64 bandwidth_bps = 4;
bytes session_key = 5;
}
message Hacker {
string handle = 1;
string real_name = 2;
int32 age = 3;
float skill_level = 4; // Fixed32
bool is_elite = 5;
int64 crew_id = 6;
repeated string exploits = 7;
repeated Tool tools = 8;
Connection active_connection = 9;
}
message Worm {
string name = 1;
int32 variant = 2;
int64 size_bytes = 3;
bytes payload = 4;
bool polymorphic = 5;
repeated string targets = 6;
}
message Operation {
string codename = 1;
string target_corp = 2;
int64 timestamp = 3;
bool successful = 4;
bytes stolen_data = 5;
repeated Hacker crew = 6;
Worm worm = 7;
repeated string log_entries = 8;
int32 severity = 9;
}
message Campaign {
string name = 1;
repeated Operation operations = 2;
int64 total_bytes_stolen = 3;
}
+477
View File
@@ -0,0 +1,477 @@
//! Generates Hackers-themed benchmark proto binaries using the roto builder API.
//!
//! Run this once to create the data files that `hackers_bench` loads:
//!
//! ```sh
//! cargo run --release --bin gen_bench_data -- --preset tiny
//! cargo run --release --bin gen_bench_data -- --preset small
//! cargo run --release --bin gen_bench_data -- --preset medium
//! cargo run --release --bin gen_bench_data -- --preset large
//! cargo run --release --bin gen_bench_data -- --preset huge
//!
//! # Custom: ~50 MB — 500 ops × 100 KB stolen_data each
//! cargo run --release --bin gen_bench_data -- \
//! --ops 500 --stolen-kb 100 --output data/bench/50mb.pb
//! ```
//!
//! Files land in `data/bench/` by default.
use clap::Parser;
use roto::hackers::{
CampaignBuilder, ConnectionBuilder, HackerBuilder, OperationBuilder, ToolBuilder, WormBuilder,
};
use std::io::{self, Write};
use std::path::Path;
// =============================================================================
// CLI
// =============================================================================
#[derive(Parser)]
#[command(
name = "gen_bench_data",
about = "Generate Hackers-themed proto binaries for benchmarks"
)]
struct Args {
/// Output file. Defaults to data/bench/<preset>.pb when --preset is used.
#[arg(short, long)]
output: Option<String>,
/// Named size preset: tiny | small | medium | large | huge
#[arg(short, long)]
preset: Option<String>,
/// Number of Operation messages.
#[arg(long, default_value_t = 100)]
ops: usize,
/// Kilobytes of random stolen_data padding per Operation.
#[arg(long, default_value_t = 0)]
stolen_kb: usize,
/// Hacker crew members per Operation.
#[arg(long, default_value_t = 3)]
crew: usize,
/// RNG seed.
#[arg(long, default_value_t = 42)]
seed: u64,
}
// =============================================================================
// Minimal xorshift64 RNG
// =============================================================================
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(if seed == 0 { 0xdeadbeef_cafebabe } else { seed })
}
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() as usize) % n
}
fn range(&mut self, lo: u64, hi: u64) -> u64 {
lo + self.next() % (hi - lo)
}
fn bool(&mut self) -> bool {
self.next() & 1 == 0
}
fn pick<'a, T>(&mut self, s: &'a [T]) -> &'a T {
&s[self.below(s.len())]
}
fn bytes(&mut self, n: usize) -> Vec<u8> {
(0..n).map(|_| self.next() as u8).collect()
}
}
// =============================================================================
// Flavour text
// =============================================================================
const HANDLES: &[&str] = &[
"Zero Cool",
"Acid Burn",
"Phantom Phreak",
"Cereal Killer",
"Lord Nikon",
"The Plague",
"Crash Override",
];
const REAL_NAMES: &[&str] = &[
"Dade Murphy",
"Kate Libby",
"Richard Gill",
"Emmanuel Goldstein",
"Paul Cook",
"Eugene Belford",
];
const EXPLOITS: &[&str] = &[
"buffer overflow",
"stack smash",
"heap spray",
"race condition",
"SQL injection",
"CSRF",
"XSS",
"RCE",
"privesc",
"kernel panic",
];
const TOOL_NAMES: &[&str] = &[
"nmap",
"metasploit",
"netcat",
"tcpdump",
"Wireshark",
"sqlmap",
"Burp Suite",
"hashcat",
"john",
];
const CORPS: &[&str] = &[
"ELLINGSON MINERAL",
"Cyberdelia",
"The Gibson",
"Prism BBS",
"Elite BBS",
"CRT Systems",
];
const LOG_LINES: &[&str] = &[
"Hack the planet!",
"Mess with the best, die like the rest.",
"I'm in.",
"They're tracing us.",
"It's a Unix system! I know this!",
"You are elite.",
"Garbage file accessed.",
];
const WORM_TARGETS: &[&str] = &[
"ELLINGSON MINERAL",
"Cyberdelia",
"The Gibson",
"Prism",
"CRT BBS",
];
const OP_NAMES: &[&str] = &[
"OPERATION HACK THE PLANET",
"GIBSON BREACH",
"ELLINGSON STING",
"WORM UNLEASHED",
"PHANTOM ACCESS",
"DA VINCI",
];
// =============================================================================
// Message builders using the generated roto::hackers API
// =============================================================================
fn gen_tool(rng: &mut Rng) -> Vec<u8> {
let payload_n = rng.range(8, 64) as usize;
let payload = rng.bytes(payload_n);
let version = format!("{}.{}", rng.range(1, 9), rng.range(0, 99));
let mut buf = vec![0u8; 512];
ToolBuilder::builder(&mut buf)
.name(*rng.pick(TOOL_NAMES))
.unwrap()
.version(&version)
.unwrap()
.payload(&payload)
.unwrap()
.is_active(rng.bool() as u64)
.unwrap()
.exploit_count(rng.range(0, 50) as i32)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_connection(rng: &mut Rng) -> Vec<u8> {
let host = format!("192.168.{}.{}", rng.range(1, 254), rng.range(1, 254));
let session_key = rng.bytes(32);
let mut buf = vec![0u8; 256];
ConnectionBuilder::builder(&mut buf)
.host(&host)
.unwrap()
.port(rng.range(1024, 65535) as i32)
.unwrap()
.encrypted(rng.bool() as u64)
.unwrap()
.bandwidth_bps(rng.range(1200, 1_000_000_000))
.unwrap()
.session_key(&session_key)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_hacker(rng: &mut Rng) -> Vec<u8> {
let tools: Vec<Vec<u8>> = (0..rng.range(1, 4)).map(|_| gen_tool(rng)).collect();
let connection = gen_connection(rng);
// Float (skill_level) is written as raw bytes by the generated builder
let skill_bits = (rng.range(10, 100) as f32 / 10.0).to_bits().to_le_bytes();
let handle = *rng.pick(HANDLES);
let real_name = *rng.pick(REAL_NAMES);
let n_exploits = rng.range(2, 5) as usize;
let exploits: Vec<&str> = (0..n_exploits).map(|_| *rng.pick(EXPLOITS)).collect();
let crew_id = rng.next();
let mut buf = vec![0u8; 8 * 1024];
let mut b = HackerBuilder::builder(&mut buf)
.handle(handle)
.unwrap()
.real_name(real_name)
.unwrap()
.age(rng.range(16, 35) as i32)
.unwrap()
.skill_level(&skill_bits)
.unwrap()
.is_elite(rng.bool() as u64)
.unwrap()
.crew_id(crew_id)
.unwrap();
for e in &exploits {
b = b.exploits(e).unwrap();
}
for t in &tools {
b = b.tools(t).unwrap();
}
b.active_connection(&connection)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_worm(rng: &mut Rng) -> Vec<u8> {
let payload = rng.bytes(64);
let name = format!("da_vinci.{}", rng.range(1, 99));
let n_targets = rng.range(1, 4) as usize;
let targets: Vec<&str> = (0..n_targets).map(|_| *rng.pick(WORM_TARGETS)).collect();
let mut buf = vec![0u8; 1024];
let mut b = WormBuilder::builder(&mut buf)
.name(&name)
.unwrap()
.variant(rng.range(1, 5) as i32)
.unwrap()
.size_bytes(rng.range(1024, 10_000_000))
.unwrap()
.payload(&payload)
.unwrap()
.polymorphic(rng.bool() as u64)
.unwrap();
for t in &targets {
b = b.targets(t).unwrap();
}
b.finish().unwrap().to_vec()
}
fn gen_operation(rng: &mut Rng, crew_count: usize, stolen_bytes: usize) -> Vec<u8> {
let crew: Vec<Vec<u8>> = (0..crew_count).map(|_| gen_hacker(rng)).collect();
let worm = gen_worm(rng);
let stolen = if stolen_bytes > 0 {
rng.bytes(stolen_bytes)
} else {
vec![]
};
let codename = *rng.pick(OP_NAMES);
let corp = *rng.pick(CORPS);
let ts = 810_000_000u64 + rng.range(0, 10_000_000);
let n_logs = rng.range(2, 6) as usize;
let logs: Vec<&str> = (0..n_logs).map(|_| *rng.pick(LOG_LINES)).collect();
// Operation buffer: crew + worm + stolen_data + small overhead
let crew_size: usize = crew.iter().map(|h| h.len() + 5).sum();
let buf_size = crew_size + worm.len() + stolen_bytes + 1024;
let mut buf = vec![0u8; buf_size];
let mut b = OperationBuilder::builder(&mut buf)
.codename(codename)
.unwrap()
.target_corp(corp)
.unwrap()
.timestamp(ts)
.unwrap()
.successful(rng.bool() as u64)
.unwrap();
if stolen_bytes > 0 {
b = b.stolen_data(&stolen).unwrap();
}
for h in &crew {
b = b.crew(h).unwrap();
}
b = b.worm(&worm).unwrap();
for l in &logs {
b = b.log_entries(l).unwrap();
}
b.severity(rng.range(1, 10) as i32)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
fn gen_campaign(
rng: &mut Rng,
op_count: usize,
crew_per_op: usize,
stolen_bytes: usize,
) -> Vec<u8> {
let ops: Vec<Vec<u8>> = (0..op_count)
.map(|i| {
if i > 0 && i % 100 == 0 {
eprintln!(
" {i}/{op_count} operations ({:.1} MB in ops so far)…",
i * (stolen_bytes + 10_000) / 1_000_000
);
}
gen_operation(rng, crew_per_op, stolen_bytes)
})
.collect();
// Pre-compute campaign buffer size: for each op the wire encoding is
// tag(1B) + varint_length(1-5B) + op_bytes
let ops_wire_size: usize = ops
.iter()
.map(|o| 1 + varint_len(o.len() as u64) + o.len())
.sum();
let mut buf = vec![0u8; ops_wire_size + 64];
let mut b = CampaignBuilder::builder(&mut buf)
.name("HACK THE PLANET CAMPAIGN")
.unwrap();
for op in &ops {
b = b.operations(op).unwrap();
}
b.total_bytes_stolen((stolen_bytes * op_count) as u64)
.unwrap()
.finish()
.unwrap()
.to_vec()
}
/// Number of bytes needed to encode `v` as a varint.
fn varint_len(mut v: u64) -> usize {
let mut n = 1usize;
while v >= 128 {
v >>= 7;
n += 1;
}
n
}
// =============================================================================
// Preset table
// =============================================================================
struct Preset {
ops: usize,
crew: usize,
stolen_kb: usize,
}
fn resolve(name: &str) -> Option<Preset> {
match name {
// ops crew stolen_kb approx size
"tiny" => Some(Preset {
ops: 1,
crew: 1,
stolen_kb: 0,
}), // ~400 B
"small" => Some(Preset {
ops: 20,
crew: 3,
stolen_kb: 0,
}), // ~25 KB
"medium" => Some(Preset {
ops: 2_000,
crew: 3,
stolen_kb: 0,
}), // ~2 MB
"large" => Some(Preset {
ops: 200,
crew: 3,
stolen_kb: 500,
}), // ~100 MB
"huge" => Some(Preset {
ops: 1_000,
crew: 3,
stolen_kb: 500,
}), // ~500 MB
_ => None,
}
}
// =============================================================================
// main
// =============================================================================
fn main() {
let mut args = Args::parse();
if let Some(ref name) = args.preset.clone() {
match resolve(name) {
Some(p) => {
args.ops = p.ops;
args.crew = p.crew;
args.stolen_kb = p.stolen_kb;
}
None => {
eprintln!("Unknown preset '{name}'. Valid: tiny, small, medium, large, huge");
std::process::exit(1);
}
}
}
let stolen_bytes = args.stolen_kb * 1024;
let approx_mb = args.ops * (700 + args.crew * 300 + stolen_bytes) / 1_000_000;
eprintln!(
"Generating: {} ops × {} crew, {} KB stolen_data each → ~{} MB",
args.ops, args.crew, args.stolen_kb, approx_mb
);
let mut rng = Rng::new(args.seed);
let data = gen_campaign(&mut rng, args.ops, args.crew, stolen_bytes);
eprintln!(
"Generated {} bytes ({:.2} MB)",
data.len(),
data.len() as f64 / 1_000_000.0
);
// Default output: data/bench/<preset>.pb, or stdout if no output and no preset
let out_path = args
.output
.clone()
.or_else(|| args.preset.as_ref().map(|p| format!("data/bench/{}.pb", p)));
match out_path {
Some(ref path) => {
if let Some(parent) = Path::new(path).parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).unwrap_or_else(|e| eprintln!("Warning: {e}"));
}
}
std::fs::write(path, &data).unwrap_or_else(|e| {
eprintln!("Error writing {path}: {e}");
std::process::exit(1);
});
eprintln!("Saved to {path}");
}
None => {
io::stdout().write_all(&data).unwrap_or_else(|e| {
eprintln!("Error: {e}");
std::process::exit(1);
});
}
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ fn map_type_to_rust_accessor(field_type: i32, label: i32) -> (String, String) {
), // TYPE_DOUBLE ), // TYPE_DOUBLE
2 => ( 2 => (
"f32".to_string(), "f32".to_string(),
"f32::from_le_bytes(bytes.try_into().map_err(|_| crate::RotoError::WireFormatViolation)?)".to_string(), "Ok(f32::from_le_bytes(bytes.try_into().map_err(|_| crate::RotoError::WireFormatViolation)?))".to_string(),
), // TYPE_FLOAT ), // TYPE_FLOAT
3 | 5 | 15 | 17 => ( 3 | 5 | 15 | 17 => (
"i32".to_string(), "i32".to_string(),
+801
View File
@@ -0,0 +1,801 @@
// @generated by protoc-gen-roto — do not edit
use crate::{ProtoAccessor, ProtoBuilder, Result, RotoError, read_varint, RepeatedFieldIterator};
use std::str;
pub struct Tool<'a> {
accessor: crate::ProtoAccessor<'a>,
name_offset: Option<usize>,
version_offset: Option<usize>,
payload_offset: Option<usize>,
is_active_offset: Option<usize>,
exploit_count_offset: Option<usize>,
}
impl<'a> Tool<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut name_offset = None;
let mut version_offset = None;
let mut payload_offset = None;
let mut is_active_offset = None;
let mut exploit_count_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { name_offset = Some(offset); }
if tag.field_number == 2 { version_offset = Some(offset); }
if tag.field_number == 3 { payload_offset = Some(offset); }
if tag.field_number == 4 { is_active_offset = Some(offset); }
if tag.field_number == 5 { exploit_count_offset = Some(offset); }
}
Ok(Self {
accessor,
name_offset,
version_offset,
payload_offset,
is_active_offset,
exploit_count_offset,
})
}
pub fn name(&self) -> crate::Result<&'a str> {
let offset = self.name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn version(&self) -> crate::Result<&'a str> {
let offset = self.version_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn payload(&self) -> crate::Result<&'a [u8]> {
let offset = self.payload_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn is_active(&self) -> crate::Result<bool> {
let offset = self.is_active_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn exploit_count(&self) -> crate::Result<i32> {
let offset = self.exploit_count_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
}
pub struct ToolBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> ToolBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> ToolBuilder<'_> {
ToolBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn version(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(2, value)?;
Ok(self)
}
pub fn payload(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(3, value)?;
Ok(self)
}
pub fn is_active(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(4, value)?;
Ok(self)
}
pub fn exploit_count(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(5, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Connection<'a> {
accessor: crate::ProtoAccessor<'a>,
host_offset: Option<usize>,
port_offset: Option<usize>,
encrypted_offset: Option<usize>,
bandwidth_bps_offset: Option<usize>,
session_key_offset: Option<usize>,
}
impl<'a> Connection<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut host_offset = None;
let mut port_offset = None;
let mut encrypted_offset = None;
let mut bandwidth_bps_offset = None;
let mut session_key_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { host_offset = Some(offset); }
if tag.field_number == 2 { port_offset = Some(offset); }
if tag.field_number == 3 { encrypted_offset = Some(offset); }
if tag.field_number == 4 { bandwidth_bps_offset = Some(offset); }
if tag.field_number == 5 { session_key_offset = Some(offset); }
}
Ok(Self {
accessor,
host_offset,
port_offset,
encrypted_offset,
bandwidth_bps_offset,
session_key_offset,
})
}
pub fn host(&self) -> crate::Result<&'a str> {
let offset = self.host_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn port(&self) -> crate::Result<i32> {
let offset = self.port_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn encrypted(&self) -> crate::Result<bool> {
let offset = self.encrypted_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn bandwidth_bps(&self) -> crate::Result<i32> {
let offset = self.bandwidth_bps_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn session_key(&self) -> crate::Result<&'a [u8]> {
let offset = self.session_key_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
}
pub struct ConnectionBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> ConnectionBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> ConnectionBuilder<'_> {
ConnectionBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn host(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn port(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(2, value)?;
Ok(self)
}
pub fn encrypted(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn bandwidth_bps(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(4, value)?;
Ok(self)
}
pub fn session_key(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(5, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Hacker<'a> {
accessor: crate::ProtoAccessor<'a>,
handle_offset: Option<usize>,
real_name_offset: Option<usize>,
age_offset: Option<usize>,
skill_level_offset: Option<usize>,
is_elite_offset: Option<usize>,
crew_id_offset: Option<usize>,
exploits_start: Option<usize>,
exploits_end: Option<usize>,
tools_start: Option<usize>,
tools_end: Option<usize>,
active_connection_offset: Option<usize>,
}
impl<'a> Hacker<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut handle_offset = None;
let mut real_name_offset = None;
let mut age_offset = None;
let mut skill_level_offset = None;
let mut is_elite_offset = None;
let mut crew_id_offset = None;
let mut exploits_start = None;
let mut exploits_end = None;
let mut tools_start = None;
let mut tools_end = None;
let mut active_connection_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { handle_offset = Some(offset); }
if tag.field_number == 2 { real_name_offset = Some(offset); }
if tag.field_number == 3 { age_offset = Some(offset); }
if tag.field_number == 4 { skill_level_offset = Some(offset); }
if tag.field_number == 5 { is_elite_offset = Some(offset); }
if tag.field_number == 6 { crew_id_offset = Some(offset); }
if tag.field_number == 7 {
if exploits_start.is_none() { exploits_start = Some(offset); }
exploits_end = Some(offset);
}
if tag.field_number == 8 {
if tools_start.is_none() { tools_start = Some(offset); }
tools_end = Some(offset);
}
if tag.field_number == 9 { active_connection_offset = Some(offset); }
}
Ok(Self {
accessor,
handle_offset,
real_name_offset,
age_offset,
skill_level_offset,
is_elite_offset,
crew_id_offset,
exploits_start, exploits_end,
tools_start, tools_end,
active_connection_offset,
})
}
pub fn handle(&self) -> crate::Result<&'a str> {
let offset = self.handle_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn real_name(&self) -> crate::Result<&'a str> {
let offset = self.real_name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn age(&self) -> crate::Result<i32> {
let offset = self.age_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn skill_level(&self) -> crate::Result<f32> {
let offset = self.skill_level_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(f32::from_le_bytes(bytes.try_into().map_err(|_| crate::RotoError::WireFormatViolation)?))
}
pub fn is_elite(&self) -> crate::Result<bool> {
let offset = self.is_elite_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn crew_id(&self) -> crate::Result<i32> {
let offset = self.crew_id_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn exploits(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.exploits_start, self.exploits_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(7, start, end),
_ => self.accessor.iter_repeated(7),
}
}
pub fn tools(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.tools_start, self.tools_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(8, start, end),
_ => self.accessor.iter_repeated(8),
}
}
pub fn active_connection(&self) -> crate::Result<&'a [u8]> {
let offset = self.active_connection_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
}
pub struct HackerBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> HackerBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> HackerBuilder<'_> {
HackerBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn handle(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn real_name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(2, value)?;
Ok(self)
}
pub fn age(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(3, value)?;
Ok(self)
}
pub fn skill_level(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(4, value)?;
Ok(self)
}
pub fn is_elite(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(5, value)?;
Ok(self)
}
pub fn crew_id(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(6, value)?;
Ok(self)
}
pub fn exploits(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(7, value)?;
Ok(self)
}
pub fn tools(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(8, value)?;
Ok(self)
}
pub fn active_connection(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(9, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Worm<'a> {
accessor: crate::ProtoAccessor<'a>,
name_offset: Option<usize>,
variant_offset: Option<usize>,
size_bytes_offset: Option<usize>,
payload_offset: Option<usize>,
polymorphic_offset: Option<usize>,
targets_start: Option<usize>,
targets_end: Option<usize>,
}
impl<'a> Worm<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut name_offset = None;
let mut variant_offset = None;
let mut size_bytes_offset = None;
let mut payload_offset = None;
let mut polymorphic_offset = None;
let mut targets_start = None;
let mut targets_end = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { name_offset = Some(offset); }
if tag.field_number == 2 { variant_offset = Some(offset); }
if tag.field_number == 3 { size_bytes_offset = Some(offset); }
if tag.field_number == 4 { payload_offset = Some(offset); }
if tag.field_number == 5 { polymorphic_offset = Some(offset); }
if tag.field_number == 6 {
if targets_start.is_none() { targets_start = Some(offset); }
targets_end = Some(offset);
}
}
Ok(Self {
accessor,
name_offset,
variant_offset,
size_bytes_offset,
payload_offset,
polymorphic_offset,
targets_start, targets_end,
})
}
pub fn name(&self) -> crate::Result<&'a str> {
let offset = self.name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn variant(&self) -> crate::Result<i32> {
let offset = self.variant_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn size_bytes(&self) -> crate::Result<i32> {
let offset = self.size_bytes_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn payload(&self) -> crate::Result<&'a [u8]> {
let offset = self.payload_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn polymorphic(&self) -> crate::Result<bool> {
let offset = self.polymorphic_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn targets(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.targets_start, self.targets_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(6, start, end),
_ => self.accessor.iter_repeated(6),
}
}
}
pub struct WormBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> WormBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> WormBuilder<'_> {
WormBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn variant(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(2, value)?;
Ok(self)
}
pub fn size_bytes(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn payload(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(4, value)?;
Ok(self)
}
pub fn polymorphic(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(5, value)?;
Ok(self)
}
pub fn targets(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(6, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Operation<'a> {
accessor: crate::ProtoAccessor<'a>,
codename_offset: Option<usize>,
target_corp_offset: Option<usize>,
timestamp_offset: Option<usize>,
successful_offset: Option<usize>,
stolen_data_offset: Option<usize>,
crew_start: Option<usize>,
crew_end: Option<usize>,
worm_offset: Option<usize>,
log_entries_start: Option<usize>,
log_entries_end: Option<usize>,
severity_offset: Option<usize>,
}
impl<'a> Operation<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut codename_offset = None;
let mut target_corp_offset = None;
let mut timestamp_offset = None;
let mut successful_offset = None;
let mut stolen_data_offset = None;
let mut crew_start = None;
let mut crew_end = None;
let mut worm_offset = None;
let mut log_entries_start = None;
let mut log_entries_end = None;
let mut severity_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { codename_offset = Some(offset); }
if tag.field_number == 2 { target_corp_offset = Some(offset); }
if tag.field_number == 3 { timestamp_offset = Some(offset); }
if tag.field_number == 4 { successful_offset = Some(offset); }
if tag.field_number == 5 { stolen_data_offset = Some(offset); }
if tag.field_number == 6 {
if crew_start.is_none() { crew_start = Some(offset); }
crew_end = Some(offset);
}
if tag.field_number == 7 { worm_offset = Some(offset); }
if tag.field_number == 8 {
if log_entries_start.is_none() { log_entries_start = Some(offset); }
log_entries_end = Some(offset);
}
if tag.field_number == 9 { severity_offset = Some(offset); }
}
Ok(Self {
accessor,
codename_offset,
target_corp_offset,
timestamp_offset,
successful_offset,
stolen_data_offset,
crew_start, crew_end,
worm_offset,
log_entries_start, log_entries_end,
severity_offset,
})
}
pub fn codename(&self) -> crate::Result<&'a str> {
let offset = self.codename_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn target_corp(&self) -> crate::Result<&'a str> {
let offset = self.target_corp_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn timestamp(&self) -> crate::Result<i32> {
let offset = self.timestamp_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn successful(&self) -> crate::Result<bool> {
let offset = self.successful_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v != 0).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn stolen_data(&self) -> crate::Result<&'a [u8]> {
let offset = self.stolen_data_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn crew(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.crew_start, self.crew_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(6, start, end),
_ => self.accessor.iter_repeated(6),
}
}
pub fn worm(&self) -> crate::Result<&'a [u8]> {
let offset = self.worm_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
Ok(bytes)
}
pub fn log_entries(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.log_entries_start, self.log_entries_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(8, start, end),
_ => self.accessor.iter_repeated(8),
}
}
pub fn severity(&self) -> crate::Result<i32> {
let offset = self.severity_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
}
pub struct OperationBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> OperationBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> OperationBuilder<'_> {
OperationBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn codename(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn target_corp(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(2, value)?;
Ok(self)
}
pub fn timestamp(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn successful(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(4, value)?;
Ok(self)
}
pub fn stolen_data(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(5, value)?;
Ok(self)
}
pub fn crew(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(6, value)?;
Ok(self)
}
pub fn worm(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(7, value)?;
Ok(self)
}
pub fn log_entries(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(8, value)?;
Ok(self)
}
pub fn severity(mut self, value: i32) -> crate::Result<Self> {
self.builder.write_int32(9, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
pub struct Campaign<'a> {
accessor: crate::ProtoAccessor<'a>,
name_offset: Option<usize>,
operations_start: Option<usize>,
operations_end: Option<usize>,
total_bytes_stolen_offset: Option<usize>,
}
impl<'a> Campaign<'a> {
pub fn new(data: &'a [u8]) -> crate::Result<Self> {
let accessor = crate::ProtoAccessor::new(data)?;
let mut name_offset = None;
let mut operations_start = None;
let mut operations_end = None;
let mut total_bytes_stolen_offset = None;
for item in accessor.fields() {
let (offset, tag, _) = item?;
if tag.field_number == 1 { name_offset = Some(offset); }
if tag.field_number == 2 {
if operations_start.is_none() { operations_start = Some(offset); }
operations_end = Some(offset);
}
if tag.field_number == 3 { total_bytes_stolen_offset = Some(offset); }
}
Ok(Self {
accessor,
name_offset,
operations_start, operations_end,
total_bytes_stolen_offset,
})
}
pub fn name(&self) -> crate::Result<&'a str> {
let offset = self.name_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
str::from_utf8(bytes).map_err(|_| crate::RotoError::WireFormatViolation)
}
pub fn operations(&self) -> crate::RepeatedFieldIterator<'a> {
match (self.operations_start, self.operations_end) {
(Some(start), Some(end)) => self.accessor.iter_repeated_range(2, start, end),
_ => self.accessor.iter_repeated(2),
}
}
pub fn total_bytes_stolen(&self) -> crate::Result<i32> {
let offset = self.total_bytes_stolen_offset.ok_or(crate::RotoError::FieldNotFound)?;
let (bytes, _) = self.accessor.get_value_at(offset)?;
crate::read_varint(bytes).map(|(v, _)| v as i32).map_err(|_| crate::RotoError::WireFormatViolation)
}
}
pub struct CampaignBuilder<'b> {
builder: crate::ProtoBuilder<'b>,
}
impl<'b> CampaignBuilder<'b> {
pub fn builder(buf: &mut [u8]) -> CampaignBuilder<'_> {
CampaignBuilder {
builder: crate::ProtoBuilder::new(buf),
}
}
pub fn name(mut self, value: &str) -> crate::Result<Self> {
self.builder.write_string(1, value)?;
Ok(self)
}
pub fn operations(mut self, value: &[u8]) -> crate::Result<Self> {
self.builder.write_bytes(2, value)?;
Ok(self)
}
pub fn total_bytes_stolen(mut self, value: u64) -> crate::Result<Self> {
self.builder.write_varint(3, value)?;
Ok(self)
}
pub fn finish(self) -> crate::Result<&'b mut [u8]> {
self.builder.finish()
}
}
+29 -11
View File
@@ -1,5 +1,6 @@
pub mod generator; pub mod generator;
pub mod google; pub mod google;
pub mod hackers;
// Uncomment this to check if the code compiles // Uncomment this to check if the code compiles
// #[path = "../proto/google/protobuf/descriptor.rs"] // #[path = "../proto/google/protobuf/descriptor.rs"]
// pub mod descriptor; // pub mod descriptor;
@@ -220,11 +221,19 @@ impl<'a> ProtoAccessor<'a> {
} }
_ => (cursor_after_tag, value_len), _ => (cursor_after_tag, value_len),
}; };
Ok((&self.data[value_offset..value_offset + actual_value_len], tag.wire_type)) Ok((
&self.data[value_offset..value_offset + actual_value_len],
tag.wire_type,
))
} }
/// Returns an iterator that scans a specific range of the buffer for all occurrences of the specified field. /// Returns an iterator that scans a specific range of the buffer for all occurrences of the specified field.
pub fn iter_repeated_range(&self, field_number: u32, start: usize, end: usize) -> RepeatedFieldIterator<'a> { pub fn iter_repeated_range(
&self,
field_number: u32,
start: usize,
end: usize,
) -> RepeatedFieldIterator<'a> {
RepeatedFieldIterator::new_range(self.data, field_number, start, end) RepeatedFieldIterator::new_range(self.data, field_number, start, end)
} }
} }
@@ -280,7 +289,11 @@ impl<'a> Iterator for FieldIterator<'a> {
self.cursor = cursor_after_tag + value_len; self.cursor = cursor_after_tag + value_len;
Some(Ok((self.cursor - tag_len - value_len, tag, &self.data[value_offset..value_offset + actual_value_len]))) Some(Ok((
self.cursor - tag_len - value_len,
tag,
&self.data[value_offset..value_offset + actual_value_len],
)))
} }
} }
@@ -293,10 +306,7 @@ pub struct RepeatedFieldIterator<'a> {
impl<'a> RepeatedFieldIterator<'a> { impl<'a> RepeatedFieldIterator<'a> {
pub fn new(data: &'a [u8], field_number: u32) -> Self { pub fn new(data: &'a [u8], field_number: u32) -> Self {
Self { Self {
iterator: FieldIterator { iterator: FieldIterator { data, cursor: 0 },
data,
cursor: 0,
},
field_number, field_number,
end_offset: None, end_offset: None,
} }
@@ -491,7 +501,8 @@ mod tests {
} }
assert_eq!(i32_vals, vec![1, 2, 3, 4, 5]); assert_eq!(i32_vals, vec![1, 2, 3, 4, 5]);
let repeated_strings: Vec<_> = acc.iter_repeated(18) let repeated_strings: Vec<_> = acc
.iter_repeated(18)
.map(|r| { .map(|r| {
let (val, _) = r.expect("Failed to decode repeated string"); let (val, _) = r.expect("Failed to decode repeated string");
std::str::from_utf8(val).expect("Invalid utf8") std::str::from_utf8(val).expect("Invalid utf8")
@@ -499,7 +510,8 @@ mod tests {
.collect(); .collect();
assert_eq!(repeated_strings, vec!["one", "two", "three"]); assert_eq!(repeated_strings, vec!["one", "two", "three"]);
let repeated_nested: Vec<_> = acc.iter_repeated(19) let repeated_nested: Vec<_> = acc
.iter_repeated(19)
.map(|r| { .map(|r| {
let (val, _) = r.expect("Failed to decode repeated nested"); let (val, _) = r.expect("Failed to decode repeated nested");
let nested_acc = ProtoAccessor::new(val).unwrap(); let nested_acc = ProtoAccessor::new(val).unwrap();
@@ -519,7 +531,8 @@ mod tests {
assert_eq!(id, 200); assert_eq!(id, 200);
// Validate that fields appear in the expected relative order // Validate that fields appear in the expected relative order
let field_numbers: Vec<u32> = acc.fields() let field_numbers: Vec<u32> = acc
.fields()
.map(|r| r.expect("Failed to decode field").1.field_number) .map(|r| r.expect("Failed to decode field").1.field_number)
.collect(); .collect();
@@ -528,7 +541,12 @@ mod tests {
let mut found_count = 0; let mut found_count = 0;
for &f in &field_numbers { for &f in &field_numbers {
if essential_fields.contains(&f) { if essential_fields.contains(&f) {
assert!(f >= last_field, "Fields appeared out of order: {} came after {}", f, last_field); assert!(
f >= last_field,
"Fields appeared out of order: {} came after {}",
f,
last_field
);
last_field = f; last_field = f;
found_count += 1; found_count += 1;
} }
+2
View File
@@ -0,0 +1,2 @@
test
hackers_bench
+22
View File
@@ -0,0 +1,22 @@
CC = cc
CFLAGS = -O2 -std=c11 -D_POSIX_C_SOURCE=199309L -I. -I/usr/include -Wall -Wextra
LDFLAGS = -lupb -lutf8_range
SRCS = hackers_bench.c hackers.upb.c hackers.upb_minitable.c
TARGET = hackers_bench
.PHONY: all clean regen
all: $(TARGET)
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
# Re-generate upb files from proto/hackers.proto
regen:
protoc -I ../proto ../proto/hackers.proto \
--upb_out=. \
--upb_minitable_out=.
clean:
rm -f $(TARGET)
+1
View File
@@ -0,0 +1 @@
File diff suppressed because it is too large Load Diff
+175
View File
@@ -0,0 +1,175 @@
/* This file was generated by upb_generator from the input file:
*
* hackers.proto
*
* Do not edit -- your changes will be discarded when the file is
* regenerated.
* NO CHECKED-IN PROTOBUF GENCODE */
#include <stddef.h>
#include "upb/generated_code_support.h"
#include "hackers.upb_minitable.h"
// Must be last.
#include "upb/port/def.inc"
extern const struct upb_MiniTable UPB_PRIVATE(_kUpb_MiniTable_StaticallyTreeShaken);
static const upb_MiniTableField Tool__fields[5] = {
{1, 16, 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, UPB_SIZE(24, 32), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(32, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{4, 8, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{5, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Tool_msg_init = {
NULL,
&Tool__fields[0],
UPB_SIZE(40, 64), 5, kUpb_ExtMode_NonExtendable, 5, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Tool",
#endif
};
const upb_MiniTable* Tool_msg_init_ptr = &Tool_msg_init;
static const upb_MiniTableField Connection__fields[5] = {
{1, 16, 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{3, 8, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{4, UPB_SIZE(32, 48), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{5, UPB_SIZE(24, 32), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Connection_msg_init = {
NULL,
&Connection__fields[0],
UPB_SIZE(40, 56), 5, kUpb_ExtMode_NonExtendable, 5, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Connection",
#endif
};
const upb_MiniTable* Connection_msg_init_ptr = &Connection_msg_init;
static const upb_MiniTableSubInternal Hacker__submsgs[2] = {
{.UPB_PRIVATE(submsg) = &Tool_msg_init_ptr},
{.UPB_PRIVATE(submsg) = &Connection_msg_init_ptr},
};
static const upb_MiniTableField Hacker__fields[9] = {
{1, UPB_SIZE(32, 24), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, 40, 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{3, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{4, 16, 0, kUpb_NoSub, 2, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{5, 9, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{6, UPB_SIZE(48, 56), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{7, UPB_SIZE(20, 64), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{8, UPB_SIZE(24, 72), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{9, UPB_SIZE(28, 80), 64, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Hacker_msg_init = {
&Hacker__submsgs[0],
&Hacker__fields[0],
UPB_SIZE(56, 88), 9, kUpb_ExtMode_NonExtendable, 9, UPB_FASTTABLE_MASK(56), 0,
#ifdef UPB_TRACING_ENABLED
"Hacker",
#endif
UPB_FASTTABLE_INIT({
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x001000003f000025, &upb_DecodeFast_Fixed32_Scalar_Tag1Byte},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
{0x0000000000000000, &_upb_FastDecoder_DecodeGeneric},
})
};
const upb_MiniTable* Hacker_msg_init_ptr = &Hacker_msg_init;
static const upb_MiniTableField Worm__fields[6] = {
{1, UPB_SIZE(20, 16), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, 12, 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(40, 48), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{4, UPB_SIZE(28, 32), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{5, 8, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{6, UPB_SIZE(16, 56), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Worm_msg_init = {
NULL,
&Worm__fields[0],
UPB_SIZE(48, 64), 6, kUpb_ExtMode_NonExtendable, 6, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Worm",
#endif
};
const upb_MiniTable* Worm_msg_init_ptr = &Worm_msg_init;
static const upb_MiniTableSubInternal Operation__submsgs[2] = {
{.UPB_PRIVATE(submsg) = &Hacker_msg_init_ptr},
{.UPB_PRIVATE(submsg) = &Worm_msg_init_ptr},
};
static const upb_MiniTableField Operation__fields[9] = {
{1, UPB_SIZE(28, 16), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, UPB_SIZE(36, 32), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(56, 64), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
{4, 9, 0, kUpb_NoSub, 8, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_1Byte << kUpb_FieldRep_Shift)},
{5, UPB_SIZE(44, 48), 0, kUpb_NoSub, 12, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{6, UPB_SIZE(12, 72), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{7, UPB_SIZE(16, 80), 64, 1, 11, (int)kUpb_FieldMode_Scalar | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{8, UPB_SIZE(20, 88), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{9, UPB_SIZE(24, 12), 0, kUpb_NoSub, 5, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_4Byte << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Operation_msg_init = {
&Operation__submsgs[0],
&Operation__fields[0],
UPB_SIZE(64, 96), 9, kUpb_ExtMode_NonExtendable, 9, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Operation",
#endif
};
const upb_MiniTable* Operation_msg_init_ptr = &Operation_msg_init;
static const upb_MiniTableSubInternal Campaign__submsgs[1] = {
{.UPB_PRIVATE(submsg) = &Operation_msg_init_ptr},
};
static const upb_MiniTableField Campaign__fields[3] = {
{1, UPB_SIZE(12, 8), 0, kUpb_NoSub, 9, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_StringView << kUpb_FieldRep_Shift)},
{2, UPB_SIZE(8, 24), 0, 0, 11, (int)kUpb_FieldMode_Array | ((int)UPB_SIZE(kUpb_FieldRep_4Byte, kUpb_FieldRep_8Byte) << kUpb_FieldRep_Shift)},
{3, UPB_SIZE(24, 32), 0, kUpb_NoSub, 3, (int)kUpb_FieldMode_Scalar | ((int)kUpb_FieldRep_8Byte << kUpb_FieldRep_Shift)},
};
const upb_MiniTable Campaign_msg_init = {
&Campaign__submsgs[0],
&Campaign__fields[0],
UPB_SIZE(32, 40), 3, kUpb_ExtMode_NonExtendable, 3, UPB_FASTTABLE_MASK(255), 0,
#ifdef UPB_TRACING_ENABLED
"Campaign",
#endif
};
const upb_MiniTable* Campaign_msg_init_ptr = &Campaign_msg_init;
static const upb_MiniTable *messages_layout[6] = {
&Tool_msg_init,
&Connection_msg_init,
&Hacker_msg_init,
&Worm_msg_init,
&Operation_msg_init,
&Campaign_msg_init,
};
const upb_MiniTableFile hackers_proto_upb_file_layout = {
messages_layout,
NULL,
NULL,
6,
0,
0,
};
#include "upb/port/undef.inc"
+42
View File
@@ -0,0 +1,42 @@
/* This file was generated by upb_generator from the input file:
*
* hackers.proto
*
* Do not edit -- your changes will be discarded when the file is
* regenerated.
* NO CHECKED-IN PROTOBUF GENCODE */
#ifndef HACKERS_PROTO_UPB_H__UPB_MINITABLE_H_
#define HACKERS_PROTO_UPB_H__UPB_MINITABLE_H_
#include "upb/generated_code_support.h"
// Must be last.
#include "upb/port/def.inc"
#ifdef __cplusplus
extern "C" {
#endif
extern const upb_MiniTable Tool_msg_init;
extern const upb_MiniTable* Tool_msg_init_ptr;
extern const upb_MiniTable Connection_msg_init;
extern const upb_MiniTable* Connection_msg_init_ptr;
extern const upb_MiniTable Hacker_msg_init;
extern const upb_MiniTable* Hacker_msg_init_ptr;
extern const upb_MiniTable Worm_msg_init;
extern const upb_MiniTable* Worm_msg_init_ptr;
extern const upb_MiniTable Operation_msg_init;
extern const upb_MiniTable* Operation_msg_init_ptr;
extern const upb_MiniTable Campaign_msg_init;
extern const upb_MiniTable* Campaign_msg_init_ptr;
extern const upb_MiniTableFile hackers_proto_upb_file_layout;
#ifdef __cplusplus
} /* extern "C" */
#endif
#include "upb/port/undef.inc"
#endif /* HACKERS_PROTO_UPB_H__UPB_MINITABLE_H_ */
+380
View File
@@ -0,0 +1,380 @@
/*
* hackers_bench.c — C/upb benchmark mirroring benches/hackers_bench.rs
*
* Proto: proto/hackers.proto
* Generated files: hackers.upb.h / .c, hackers.upb_minitable.h / .c
*
* Build: make
* Run: ./hackers_bench
*
* Data files are read from ../data/bench/<name>.pb — the same files
* produced by `cargo run --release --bin gen_bench_data -- --preset <name>`.
*
* The four benchmark groups match the Rust/criterion groups exactly:
*
* shallow_parse — Campaign_parse() + Arena_Free() per iteration.
* upb fully decodes the message; roto merely scans
* for field offsets. This is the most important
* comparison: total cost to "be ready to read".
*
* deep_parse — parse + walk Campaign → Operations → every Hacker,
* touching each Hacker's handle field.
*
* field_access — message pre-parsed once outside the loop; each
* micro-benchmark times a single field read.
* upb: direct struct lookup. roto: decode at offset.
*
* iterate — count_operations: parse + count top-level repeated.
* count_all_crew: parse + count nested repeated.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#include "hackers.upb.h"
#include "hackers.upb_minitable.h"
/* ==========================================================================
* Timing
* ========================================================================== */
static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
}
/* ==========================================================================
* Black-box sink — prevents the compiler from optimising away benchmark work.
* We write the result of every meaningful computation here.
* ========================================================================== */
static volatile uintptr_t g_sink;
/* ==========================================================================
* File I/O
* ========================================================================== */
typedef struct {
uint8_t *data;
size_t len;
char path[256];
} BenchData;
static bool load_bench_data(BenchData *out, const char *name) {
snprintf(out->path, sizeof(out->path), "../data/bench/%s.pb", name);
FILE *f = fopen(out->path, "rb");
if (!f) {
printf("[skip] %s not found — "
"run `cargo run --release --bin gen_bench_data -- --preset %s` first\n",
out->path, name);
out->data = NULL;
out->len = 0;
return false;
}
fseek(f, 0, SEEK_END);
out->len = (size_t)ftell(f);
rewind(f);
out->data = malloc(out->len);
if (!out->data) { fclose(f); return false; }
fread(out->data, 1, out->len, f);
fclose(f);
return true;
}
static void free_bench_data(BenchData *d) {
free(d->data);
d->data = NULL;
}
/* ==========================================================================
* Benchmark runner
*
* Finds a batch size such that one batch takes ≥1 ms, then runs batches
* until at least BENCH_MIN_SECS of wall time has elapsed. Reports the
* mean ns/iter and, if bytes > 0, the MB/s throughput.
* ========================================================================== */
#define BENCH_MIN_SECS 0.5
typedef void (*bench_fn)(void *state);
static void run_bench(bench_fn fn, void *state, size_t bytes, const char *label) {
/* warmup */
for (int i = 0; i < 5; i++) fn(state);
/* calibrate: find batch size so one batch ≥ 1 ms */
uint64_t batch = 1;
while (batch < 10000000ULL) {
uint64_t t0 = now_ns();
for (uint64_t i = 0; i < batch; i++) fn(state);
if (now_ns() - t0 >= 1000000ULL) break; /* 1 ms */
batch *= 4;
}
/* measure */
uint64_t target_ns = (uint64_t)(BENCH_MIN_SECS * 1e9);
uint64_t total_ns = 0;
uint64_t total_its = 0;
while (total_ns < target_ns) {
uint64_t t0 = now_ns();
for (uint64_t i = 0; i < batch; i++) fn(state);
total_ns += now_ns() - t0;
total_its += batch;
}
double ns_per_iter = (double)total_ns / (double)total_its;
if (bytes > 0) {
double mb_per_sec = (double)bytes / ns_per_iter * 1000.0;
printf(" %-46s %9.2f ns/iter %8.2f MB/s\n",
label, ns_per_iter, mb_per_sec);
} else {
printf(" %-46s %9.2f ns/iter\n", label, ns_per_iter);
}
}
/* ==========================================================================
* shallow_parse — Campaign_parse() + upb_Arena_Free() per iteration
*
* Measures the full cost of becoming "ready to access any field", matching
* the Rust `Campaign::new()` benchmark. upb fully decodes; roto only scans.
* ========================================================================== */
static void fn_shallow_parse(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
g_sink = (uintptr_t)c;
upb_Arena_Free(arena);
}
static void bench_shallow_parse(void) {
const char *sizes[] = {"tiny", "small", "medium", "large", NULL};
printf("\n=== shallow_parse ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign_parse/%s [%zu B]", sizes[i], d.len);
run_bench(fn_shallow_parse, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* deep_parse — parse + walk Campaign → Operations → Hackers
*
* After Campaign_parse(), upb has already decoded everything. The "deep"
* walk is pointer-chasing through the decoded tree. In roto each level
* calls ::new(), paying another linear scan over that sub-message's bytes.
* ========================================================================== */
static void fn_deep_parse(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n_ops;
const Operation * const *ops = Campaign_operations(c, &n_ops);
size_t hacker_count = 0;
for (size_t i = 0; i < n_ops; i++) {
size_t n_crew;
const Hacker * const *crew = Operation_crew(ops[i], &n_crew);
for (size_t j = 0; j < n_crew; j++) {
upb_StringView handle = Hacker_handle(crew[j]);
g_sink = (uintptr_t)handle.data;
hacker_count++;
}
}
g_sink = hacker_count;
upb_Arena_Free(arena);
}
static void bench_deep_parse(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== deep_parse ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "Campaign+Ops+Hackers/%s [%zu B]", sizes[i], d.len);
run_bench(fn_deep_parse, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* field_access — individual field reads on a pre-parsed message
*
* Parse once outside the loop; each micro-benchmark measures the accessor
* call itself. upb: a struct-field read with a MiniTable lookup.
* roto: decode the value at a pre-recorded byte offset.
* ========================================================================== */
typedef struct {
upb_Arena *arena;
Campaign *campaign;
Operation *op;
Hacker *hacker;
Worm *worm;
} FieldState;
static void fn_field_campaign_name(void *s) {
upb_StringView v = Campaign_name(((FieldState *)s)->campaign);
g_sink = (uintptr_t)v.data;
}
static void fn_field_total_bytes_stolen(void *s) {
g_sink = (uintptr_t)(uint64_t)Campaign_total_bytes_stolen(((FieldState *)s)->campaign);
}
static void fn_field_op_codename(void *s) {
upb_StringView v = Operation_codename(((FieldState *)s)->op);
g_sink = (uintptr_t)v.data;
}
static void fn_field_op_timestamp(void *s) {
g_sink = (uintptr_t)(uint64_t)Operation_timestamp(((FieldState *)s)->op);
}
static void fn_field_op_successful(void *s) {
g_sink = (uintptr_t)Operation_successful(((FieldState *)s)->op);
}
static void fn_field_hacker_handle(void *s) {
upb_StringView v = Hacker_handle(((FieldState *)s)->hacker);
g_sink = (uintptr_t)v.data;
}
static void fn_field_hacker_skill_level(void *s) {
/* store float bits to avoid FPU → int conversion costs */
float f = Hacker_skill_level(((FieldState *)s)->hacker);
uint32_t bits; memcpy(&bits, &f, 4);
g_sink = bits;
}
static void fn_field_hacker_is_elite(void *s) {
g_sink = (uintptr_t)Hacker_is_elite(((FieldState *)s)->hacker);
}
static void fn_field_worm_polymorphic(void *s) {
g_sink = (uintptr_t)Worm_polymorphic(((FieldState *)s)->worm);
}
static void fn_field_worm_payload(void *s) {
upb_StringView v = Worm_payload(((FieldState *)s)->worm);
g_sink = (uintptr_t)v.data;
}
static void bench_field_access(void) {
BenchData d;
if (!load_bench_data(&d, "small")) return;
upb_Arena *arena = upb_Arena_New();
Campaign *campaign = Campaign_parse((const char *)d.data, d.len, arena);
if (!campaign) { fprintf(stderr, "parse failed\n"); return; }
size_t n_ops;
const Operation * const *ops = Campaign_operations(campaign, &n_ops);
if (n_ops == 0) { fprintf(stderr, "no operations\n"); return; }
Operation *op = (Operation *)ops[0]; /* cast away const for state */
size_t n_crew;
const Hacker * const *crew = Operation_crew(op, &n_crew);
if (n_crew == 0) { fprintf(stderr, "no crew\n"); return; }
Hacker *hacker = (Hacker *)crew[0];
const Worm *worm = Operation_worm(op);
if (!worm) { fprintf(stderr, "no worm\n"); return; }
FieldState state = {
.arena = arena,
.campaign = campaign,
.op = op,
.hacker = hacker,
.worm = (Worm *)worm,
};
printf("\n=== field_access ===\n");
run_bench(fn_field_campaign_name, &state, 0, "campaign::name");
run_bench(fn_field_total_bytes_stolen, &state, 0, "campaign::total_bytes_stolen");
run_bench(fn_field_op_codename, &state, 0, "operation::codename");
run_bench(fn_field_op_timestamp, &state, 0, "operation::timestamp");
run_bench(fn_field_op_successful, &state, 0, "operation::successful");
run_bench(fn_field_hacker_handle, &state, 0, "hacker::handle");
run_bench(fn_field_hacker_skill_level, &state, 0, "hacker::skill_level (f32)");
run_bench(fn_field_hacker_is_elite, &state, 0, "hacker::is_elite (bool)");
run_bench(fn_field_worm_polymorphic, &state, 0, "worm::polymorphic (bool)");
run_bench(fn_field_worm_payload, &state, 0, "worm::payload (bytes)");
upb_Arena_Free(arena);
free_bench_data(&d);
}
/* ==========================================================================
* iterate — count repeated fields at different depths
*
* count_operations: after parsing, Campaign_operations() returns pointer+count
* in O(1) — upb already decoded the array.
* roto's Campaign::new() scan IS the counting work.
*
* count_all_crew: parse + walk ops + sum crew sizes.
* ========================================================================== */
static void fn_count_operations(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n;
Campaign_operations(c, &n);
g_sink = n;
upb_Arena_Free(arena);
}
static void fn_count_all_crew(void *state) {
BenchData *d = state;
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_parse((const char *)d->data, d->len, arena);
size_t n_ops;
const Operation * const *ops = Campaign_operations(c, &n_ops);
size_t total = 0;
for (size_t i = 0; i < n_ops; i++) {
size_t n_crew;
Operation_crew(ops[i], &n_crew);
total += n_crew;
}
g_sink = total;
upb_Arena_Free(arena);
}
static void bench_iterate(void) {
const char *sizes[] = {"tiny", "small", "medium", NULL};
printf("\n=== iterate ===\n");
for (int i = 0; sizes[i]; i++) {
BenchData d;
if (!load_bench_data(&d, sizes[i])) continue;
char label[80];
snprintf(label, sizeof(label), "count_operations/%s [%zu B]", sizes[i], d.len);
run_bench(fn_count_operations, &d, d.len, label);
snprintf(label, sizeof(label), "count_all_crew/%s [%zu B]", sizes[i], d.len);
run_bench(fn_count_all_crew, &d, d.len, label);
free_bench_data(&d);
}
}
/* ==========================================================================
* main
* ========================================================================== */
int main(void) {
printf("hackers_bench (upb / protobuf %s)\n", "33.1");
printf("Data files: ../data/bench/<name>.pb\n");
printf("Run `cargo run --release --bin gen_bench_data -- --preset <name>` to generate.\n");
bench_shallow_parse();
bench_deep_parse();
bench_field_access();
bench_iterate();
printf("\n");
return 0;
}
+13
View File
@@ -0,0 +1,13 @@
#include <stdio.h>
#include <stdlib.h>
#include "hackers.upb.h"
#include "hackers.upb_minitable.h"
int main(void) {
upb_Arena *arena = upb_Arena_New();
Campaign *c = Campaign_new(arena);
(void)c;
printf("name: %.*s\n", (int)Campaign_name(c).size, Campaign_name(c).data);
upb_Arena_Free(arena);
return 0;
}