What the timer is allowed to measure
This page is the suite’s timing contract. Every language runner and every serializer adapter must follow it. Architecture describes the loop. This page says which work belongs in each step.
If two adapters on the same Dashboard slice time different work, the rank is not a library comparison. The google-protobuf JavaScript row once encoded in prepare and timed a Buffer copy. That was a contract violation, not a fast encoder.
The three steps
The runner calls three methods. Only two are timed.
| Step | When | Timed? | Allowed work | Forbidden work |
|---|---|---|---|---|
| prepare | Once per cell, before the loop | No | Load a schema. Build or compile an encoder. Bind a type-specific function. Convert the suite value into the library’s in-memory message (a struct, a generated protobuf object, a Document DOM). Allocate reusable buffers. |
Write the output bytes. Cache a finished encoding and hand it back later. |
| serialize | Every repetition | Yes | Walk that in-memory message and emit bytes (or the language’s in-memory equivalent). | Return a copy of bytes written in prepare. Rebuild the schema. Re-bind the type. |
| deserialize | Every repetition | Yes | Read those bytes and rebuild the library’s in-memory message. | Skip the library and parse with a second library “for convenience,” then ignore the first result. |
Fidelity (does the value still mean the same thing?) runs after deserialize. It is not timed.
suite value
│
▼
prepare untimed suite value → library message
│
▼
serialize TIMED library message → bytes
│
▼
deserialize TIMED bytes → library message
│
▼
to-domain untimed library message → suite value (when the runner has this hook)
│
▼
fidelity untimed compare suite values
One sentence
The timer measures encode and decode. It does not measure setup. It does not measure a copy of work that already finished.
What “library message” means
The library’s normal in-memory type:
- Protocol Buffers: a generated
Message, a protobufjs message, aBinaryWriterinput object already built - JSON with a DOM library: a
Document/JsonDocument/nlohmann::jsonobject - JSON with
JSON.stringify: the plain JavaScript object (there is no other native type) - Speedy / Bitsery / custom-binary: the language struct itself
Building that object is prepare. Writing it out is serialize.
Hard rules
-
Do not cache output bytes in
prepare.
Ifserializeonly copies astring,Vec<u8>, orBufferthatpreparealready filled, the encode column is a memcpy. That is a bug. Parse-only libraries (simdjson has no encoder) must still write JSON duringserialize, or the row must not report an encode time as if it were that library. -
Do not time schema compile, type registration, or
MakeGenericMethod.
Those belong inprepare. If the first timed call still builds the serializer, the first repetition is warmup (dropped) but later reps must not rebuild it. -
Domain ↔ library maps are untimed when the runner can do it — except a 401 pair that must share one end-to-end path.
C#, Go, and most Java/Swift rows convert back to the suite value after the timer. The Java Protostuff / protobuf-java pair and the Swift FlatBuffers / SwiftProtobuf pair time suite value → bytes → suite value on both sides, because one library’s native type is the suite object. -
Both adapters in one 401 comparison must time the same kind of work.
Those two pairs now share the suite-value path. Do not revert one adapter to “prepared native only” without changing the other. -
Encode N instances when the cell says N.
DataTypeInstanceCount=100means the timed call encodes 100 values (or one documented batch frame of 100). Encoding one value and writing100is a critical bug. -
Stream mode must be a different API, or it must be labelled
adapted.
Bytes-then-writeis not a native stream path. -
The row name must match the timed functions.
A row namedsimdjsonmay use simdjson on decode. If encode isnlohmann::json::dump, the article and the inventory must say so. A row namedgoogle-protobufmust call that library’s writer on the timed encode. -
Columnar layouts and SBE keep row-to-layout conversion on the clock.
Fortable,table_project,nested_table, andsignal, building an Arrow record batch, Parquet or ORC columns, or an SBE flyweight from suite rows is serialize work. Schema objects, writer properties, and SBE codegen stay inprepareor in the build. Pre-building the batch inprepareand timing a byte copy hides the cost this family measures. This overrides rule 3 for these four type ids only.
TimeDeserfortable,nested_table, andsignalmaterializes domain rows.TimeDeserfortable_projectmaterializesf_float_0only. Row peers may fully decode and then slice that column. That decode stays on the clock. -
Array types time the file, not the source fill.
Forgridandgrid_window, building the float64 array is prepare. Creating the file, writing the whole array, and reading either the whole array or the window are timed. Declaring the ADIOS IO and selecting the BP5 engine is prepare.grid_windowdeserialize is a hyperslab, a NetCDFstart/count, or an ADIOS selection. A full read plus a host-side slice is not agrid_windowresult. The size column is the full file.
Allowed exceptions (must be labelled)
| Exception | Why it exists | How to label it |
|---|---|---|
| Parse-only library (no public encoder) | simdjson is a parser | Encode is another JSON writer, timed. Decode is the parser. The 401 simdjson page is the model. |
| Columnar and SBE layout conversion | Shredding rows into columns, or packing an SBE body, is the format | Rule 8. Schema compile stays in prepare. The conversion stays in serialize. table_project deserialize returns one column. |
C# in-memory path is a string |
Many .NET APIs return strings | Binary codecs often Base64 that string. Both sides of a C# pair must do the same. Do not compare those nanoseconds to a Rust Vec<u8> row. |
| Envelope / teaching wrapper | C ubj wraps custom-binary |
The 401 page must say the envelope is the measured extra work. |
| In-place layout used as a classical decoder | rkyv from_bytes builds an owned struct |
The 401 rkyv page must say access is not timed. |
| 401 pair whose other library has no separate native type | Protostuff and FlatBuffers encode the suite object | Both sides time suite value → bytes → suite value. |
If you need a new exception, write it on the language Overview and next to the Dashboard claim. Do not invent a silent one.
How to check one adapter
Open the adapter. Answer four questions. All four must be yes, or the row is dishonest.
- Does
preparestop before any function that returns the encoded payload? - Does timed
serializecall the library’s encode / dump / write / toBinary / SerializeToArray (or the documented stand-in)? - Does timed
deserializecall the library’s decode / parse / read / fromBinary / ParseFromArray? - If this adapter is compared with another in a 401 article, do both time the same kind of object (suite value vs prepared native message) on both encode and decode?
Audit of the 401 comparison pairs
Checked against the adapters in this repository after the google-protobuf encode fix.
| Article | What the two timed paths actually do | Contract |
|---|---|---|
| Python: orjson vs json | Both dump/load a prepared dict | Same work. Honest. |
| Python: msgspec-msgpack vs orjson | Struct encode vs dict JSON; maps untimed | Different layouts, same clock rule. Honest. |
| Python: msgspec JSON vs MessagePack | Same Struct, two encoders | Same work. Honest. |
| Rust: Speedy vs Bincode | Speedy writes a Document. Bincode writes a Fixture enum through Serde |
Wrapper asymmetry, already stated in the article. |
| Rust: Speedy vs Postcard | Same asymmetry as above | Documented. |
| Rust: rkyv vs Speedy | Both fill an owned Document; rkyv does not use access |
Documented. Honest for that choice. |
| C: custom-binary vs ubj | ubj encodes custom-binary, then an envelope | Documented. Honest for that choice. |
| C++: Bitsery vs YAS | Both encode the C++ value | Same work. Honest. |
| C++: simdjson | Encode is nlohmann dump of a prepared object. Decode is simdjson parse, then a DOM walk |
Encode is not simdjson. Must stay labelled. |
| C#: BinaryPack vs Bond Fast | Both encode then Base64 on the string path | Same extra work. Honest within C#. |
| Go: kelindar vs Avro | Both encode domain structs | Same work. Honest. |
| Java: Protostuff vs protobuf-java | Both time suite Document → bytes → suite Document. protobuf-java still builds a generated message in the middle |
Same work at the suite boundary. |
| Kotlin: Protostuff vs protobuf | Both time suite Document → bytes → suite Document. Official protobuf rows still build a generated message in the middle |
Same work at the suite boundary. |
| Kotlin: kotlinx-json vs Moshi | Both dump/load a suite Document as named JSON |
Same work. Honest. |
| Kotlin: FlatBuffers vs protobuf | Both time suite Document → bytes → suite Document |
Same work at the suite boundary. |
| PHP: JSON vs protobuf | Both time suite arrays → bytes → suite arrays. protobuf still builds a generated message in the middle | Same work at the suite boundary. |
| Fortran: json-fortran vs rojff | Both time a suite document → compact JSON text → suite document | Same named JSON. Integers outside int32 are JSON numbers on both rows. |
| Zig: std.json vs serde.json | Both time suite structs → JSON bytes → suite structs | Same comptime @typeInfo idea, two libraries. |
| Mojo: EmberJson vs mojo-avro | EmberJson times serialize / deserialize. Avro times encode / decode on AvroDatum. |
Same suite Document; JSON text vs Avro binary. |
Zig protobuf (Arwalk/zig-protobuf) |
Prepare copies suite → generated message. Timed path is encode / decode. Domain copy-back is after decode (same shape as Rust prost) |
Honest schema row. |
Zig flatbuffers |
Prepare stores fixtures. Timed path is Builder.writeTable / decodeRoot |
Honest schema row. |
Zig capnproto |
Prepare fills C structs. Timed path is official C++ MallocMessageBuilder / FlatArrayMessageReader |
Honest schema row (C++ runtime, same as Swift). |
C++ capnproto |
Prepare fills MallocMessageBuilder. Timed path is messageToFlatArray / writeMessage and FlatArrayMessageReader / InputStreamMessageReader. to_domain walks fields |
Honest schema row (same split as C++ protobuf). |
| Dagr, seven languages | Prepare builds the direct-builder value or the arena. Timed serialize is direct::build, Arena::serialize, or to_bytes (every field is written). Timed deserialize materializes the suite value. N=1 to N=100 size ratio is about 100, and the same layout has the same size across languages |
Honest schema row. A latency win for dagr-frozen-packed on message is that packed encode, not a buffer cached in prepare. |
| JavaScript: JSON vs google-protobuf | Both encode and decode on the clock | Same work. Honest (after the copy bug was fixed). |
| JavaScript: three protobufs | All three encode in serialize. Decode rebuilds a library message; toDomain copies after the timer |
Encode is the same kind of work. Decode implementations still differ. |
| Swift: FlatBuffers vs SwiftProtobuf | Both time suite Document → bytes → suite Document |
Same work at the suite boundary. |
Remaining contract gaps (not 401 pairs)
These rows violate the contract or mix two contracts. They are listed so a later change can close them. They are not silent.
| Gap | Where | What is wrong |
|---|---|---|
| Domain map on the clock | C protobuf to_proto / from_proto on every timed call |
The C runner passes a distinct fixture per instance, so convert-in-prepare cannot cover N>1 without a prepare_many hook. Rust prost now converts every instance in untimed prepare_many. |
| Fixture vs inner struct | Rust Serde rows vs Speedy / rkyv / minicbor | Same as the 401 Speedy articles. Documented, not a silent bug. |
| Envelope instead of native model | C avro-c, flatcc, ubj | Timed path wraps custom-binary. Teaching rows; see the C 401 article. |
| C nanopb / protobuf-c / protobuf-wire | C schema rows | Same in-tree pb_v2_encode body under three names. Wiring the real libraries is a separate implementation task. |
| Some C# XML/JSON ctors still lazy | DataContract XML, XmlSerializer | Warmup drops rep 0. Bond now builds in Initialize. |