Skip to content

A metadata record is 200 bytes, and 72 of them buy a replication property - #217

Merged
matrixarkai merged 1 commit into
mainfrom
perf/narrow-the-metadata-record
Oct 24, 2026
Merged

matrixarkai merged 1 commit into
mainfrom
perf/narrow-the-metadata-record

Conversation

@matrixarkai

Copy link
Copy Markdown
Owner

The metaserver's share of the metadata line is 200 bytes an object. This prices every
part of it by construction, and records which parts are available.

bytes
map key's SharedText handle 16
MetadataRecord.key 16 a second handle to the same characters
RecordValue 16 a tag and a box
version, update_time_ms, deleted 24
key characters object/{bucket}/{key} 56 40 chars + a 16-byte Arc header
boxed ObjectProjection 72
200

What a record's value actually is

Nothing reads a projection's fields. Its whole surface is as_text, len, is_empty
and an equality that compares the rendered characters — which the type says itself: two
values that spell the same thing are the same value.

So the projection is not a restatement of the object sitting beside its real copy. It is
the record's value
, held as thirteen fields instead of the 227 characters they render to.

Why it cannot become a locator

A record whose value located the object instead of describing it would be far smaller — a
handle and a few scalars, twenty-four bytes against eighty-eight. That shape is not available
here, and the reason is not taste.

A record's rendered text goes into the snapshot checksum and the transaction proofs:

for (key, record) in &self.records {
    append_string(&mut material, key);
    append_string(&mut material, &record.value.as_text());

and the function that turns a record into bytes is
append_metadata_record(out: &mut Vec<u8>, record: &MetadataRecord) — a pure function of a
record, with no store in reach and no business having one.

If an object record rendered its text by consulting the storage node, the checksum over a set
of records would become a function of the records and whatever the local storage node
currently said
. Two replicas whose storage nodes differ by one in-flight write would hash
different material for identical records. That is the one failure a replicated log exists to
rule out, and this tree refuses trailing bytes in a command for exactly the same reason.

So those 72 bytes buy the property that a record's proof is a function of the record
alone.
They are load-bearing.

What is available, measured

candidate saves
a. the content type as an index 8 — blocked
b. the three counts as 16-bit 0
c. the retention stamps behind a pointer 8
d. all three together 24
e. the clean subset, b and c only 8

(a) is blocked the way it is on the object side: the shared content-type table is bounded by
a count of 256, an object past the bound owns its own string, and a u16 cannot say
which — an enum over the two cases is sixteen bytes again. Without it the other two collapse
no further padding than (c) does alone, so the clean figure is 8 and not 16.

(b) saving nothing is worth stating on its own. Three u32 counts narrowed to u16 free
twelve bytes of field and zero bytes of struct, because they were already in padding.

the clean narrowing 200 → 192
with the duplicate key handle and its Arc header dropped → 160

And that second one is not a migration — which I had wrong until I read the encoder. The
snapshot writes the map key, not the record's:

for (key, record) in &self.records {
    append_string(&mut out, key);
    append_string(&mut out, &record.value.as_text());

Only the transaction proof writes record.key, through
append_metadata_record(out, record), and it writes the key then the value — so a
signature taking the key from the caller instead of the record produces byte-identical
material. What it costs is the 32 Vec<MetadataRecord> signatures that carry a record
without its key: a refactor, not a format change.

The Arc header is a consequence, not a choice

The key's characters cost 56 for 40 characters; sixteen of that is an Arc header, paid
because MetadataRecord holds a second handle to the same allocation. A key nothing shares
wants Box<str>, which charges no header. The header and the duplicate are one item, not
two
— which is why they are priced together above.

Measured

  • Suite: 407 passing on the default feature set, 428 with every feature, the same 21 failures in both and name for name the baseline in tools/known_failures.txt
  • cargo build --all-targets --locked and --all-features, RUSTFLAGS=-D warnings — green
  • cargo doc --no-deps --locked and --all-features, RUSTDOCFLAGS=-D warnings — green

🤖 Generated with Claude Code

…erty

The metaserver's share of the metadata line is 200 bytes an object. This prices
every part of it, by construction, and records which parts are available:

   16  the map key's `SharedText` handle
   16  `MetadataRecord.key` -- a SECOND handle to the same characters
   16  `RecordValue` -- a tag and a box
   24  version, update_time_ms, deleted
   56  the key's characters, `object/{bucket}/{key}`: 40 chars + a 16-byte Arc header
   72  the boxed `ObjectProjection`
  ---
  200

WHAT A RECORD'S VALUE ACTUALLY IS

Nothing reads a projection's FIELDS. Its whole surface is `as_text`, `len`,
`is_empty` and an equality that compares the rendered characters -- which the type
says itself: two values that spell the same thing are the same value. So the
projection is not a restatement of the object sitting beside its real copy. IT IS
THE RECORD'S VALUE, held as thirteen fields instead of the 227 characters they
render to.

WHY IT CANNOT BECOME A LOCATOR

A record whose value located the object instead of describing it would be far
smaller -- a handle and a few scalars, twenty-four bytes against eighty-eight.
That shape is not available here, and the reason is not taste.

A record's rendered text goes into the snapshot checksum and into the transaction
proofs:

    for (key, record) in &self.records {
        append_string(&mut material, key);
        append_string(&mut material, &record.value.as_text());

and the function that turns a record into bytes is
`append_metadata_record(out: &mut Vec<u8>, record: &MetadataRecord)` -- a pure
function of a record, with no store in reach and no business having one.

If an object record rendered its text by consulting the storage node, the
checksum over a set of records would become a function of the records AND
whatever the local storage node currently said. TWO REPLICAS WHOSE STORAGE NODES
DIFFER BY ONE IN-FLIGHT WRITE WOULD HASH DIFFERENT MATERIAL FOR IDENTICAL
RECORDS. That is the one failure a replicated log exists to rule out, and this
tree refuses trailing bytes in a command for exactly the same reason.

So the 72 bytes buy the property that A RECORD'S PROOF IS A FUNCTION OF THE
RECORD ALONE. They are load-bearing.

WHAT IS AVAILABLE, MEASURED

  a. the content type as an index                8   BLOCKED
  b. the three counts as 16-bit                  0
  c. the retention stamps behind a pointer       8
  d. all three together                         24
  e. the clean subset, b and c only              8

(a) is blocked the way it is blocked on the object side: the shared content-type
table is bounded by a COUNT of 256, an object past the bound owns its own string,
and a `u16` cannot say which -- an enum over the two cases is sixteen bytes again.
Without it the other two collapse no further padding than (c) does alone, so the
clean figure is 8 and not 16.

(b) saving nothing on its own is worth stating: three `u32` counts narrowed to
`u16` free twelve bytes of field and zero bytes of struct, because they were
already sitting in padding.

  the clean narrowing          200 -> 192
  with the duplicate key handle and its Arc header dropped  -> 160

AND THAT SECOND ONE IS NOT A MIGRATION, which I had wrong until I read the
encoder. The SNAPSHOT writes the MAP key, not the record's:

    for (key, record) in &self.records {
        append_string(&mut out, key);
        append_string(&mut out, &record.value.as_text());

Only the transaction proof writes `record.key`, through
`append_metadata_record(out, record)`, and it writes the key THEN the value -- so a
signature taking the key from the caller instead of the record produces
BYTE-IDENTICAL material. What it costs is the 32 `Vec<MetadataRecord>` signatures
that carry a record without its key, which is a refactor and not a format change.

THE ARC HEADER IS A CONSEQUENCE, NOT A CHOICE

The key's characters cost 56 for 40 characters: sixteen of that is an `Arc`
header, paid because `MetadataRecord` holds a second handle to the same
allocation. A key nothing shares wants `Box<str>`, which charges no header. The
header and the duplicate are one item, not two.

MEASURED

Suite: 407 passing on the default feature set and 428 with every
feature, with the same 21 failures in both, name for name the baseline in
tools/known_failures.txt. Green on cargo build --all-targets for both feature sets with
warnings denied, and on cargo doc for both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@matrixarkai
matrixarkai merged commit 48ec88f into main Oct 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants