builder: eager-reserve + value ObjectBuilder — zero-defer, zero OB alloc

Decomplect that unlocks a perf win. SetBytes deferred its data into an offsets
[]offsetEntry slice replayed in Finish — machinery that recorded intentions to
replay later, and heap-allocated a slice per object-with-a-bytes-field. Replace
it with directness: StartObject RESERVES the fixed section eagerly, so SetBytes
appends its tail + patches its own pointer on the spot; Finish just returns the
offset. Removing the offsets field makes ObjectBuilder's methods mutate only
ob.b (through the pointer) and never ob itself — so ObjectBuilder becomes a
VALUE type and StartObject stops heap-allocating &ObjectBuilder per call (it was
65% of composite-build allocs).

Byte-identical wire (golden + fuzz + pool byte-equality + nested-proof suites all
green). Real X-tx wire composite: 922ns/11allocs -> 655ns/5allocs (1.4x, <half
the allocs); vs the original byte-blob 2551ns/37allocs that's 3.9x / 7.4x fewer
allocs. Speeds EVERY zap object build node-wide, not just X.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
zeekay
2026-07-14 09:34:28 -07:00
co-authored by Hanzo Dev
parent 6f9fb0bfef
commit f9909a847a
2 changed files with 57 additions and 76 deletions
+49 -68
View File
@@ -95,27 +95,30 @@ type ObjectBuilder struct {
b *Builder
startPos int
dataSize int
offsets []offsetEntry
}
type offsetEntry struct {
fieldOffset int
targetPos int // positive = known position, negative = deferred write
data []byte // actual bytes to write (deferred text/bytes)
}
// StartObject starts building an object with the given data size.
func (b *Builder) StartObject(dataSize int) *ObjectBuilder {
// StartObject starts building an object with the given data size, RESERVING the
// full fixed section up front (zero-filled). Reserving eagerly means a variable
// field (SetBytes/SetText) can append its tail data immediately after the fixed
// section and patch its own relative pointer on the spot — there is no deferred
// offset list to record intentions and replay in Finish. Callers already build
// child objects/lists BEFORE StartObject (the writeXxxList-then-StartObject
// pattern), so the object's own Set* calls are the only buffer writes between
// StartObject and Finish; appending tail bytes immediately is safe and produces
// byte-identical wire.
func (b *Builder) StartObject(dataSize int) ObjectBuilder {
b.align(Alignment)
return &ObjectBuilder{
ob := ObjectBuilder{
b: b,
startPos: b.pos,
dataSize: dataSize,
}
ob.ensureField(dataSize)
return ob
}
// SetBool sets a bool field.
func (ob *ObjectBuilder) SetBool(fieldOffset int, v bool) {
func (ob ObjectBuilder) SetBool(fieldOffset int, v bool) {
if v {
ob.SetUint8(fieldOffset, 1)
} else {
@@ -124,56 +127,56 @@ func (ob *ObjectBuilder) SetBool(fieldOffset int, v bool) {
}
// SetUint8 sets a uint8 field.
func (ob *ObjectBuilder) SetUint8(fieldOffset int, v uint8) {
func (ob ObjectBuilder) SetUint8(fieldOffset int, v uint8) {
ob.ensureField(fieldOffset + 1)
ob.b.buf[ob.startPos+fieldOffset] = v
}
// SetUint16 sets a uint16 field.
func (ob *ObjectBuilder) SetUint16(fieldOffset int, v uint16) {
func (ob ObjectBuilder) SetUint16(fieldOffset int, v uint16) {
ob.ensureField(fieldOffset + 2)
binary.LittleEndian.PutUint16(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetUint32 sets a uint32 field.
func (ob *ObjectBuilder) SetUint32(fieldOffset int, v uint32) {
func (ob ObjectBuilder) SetUint32(fieldOffset int, v uint32) {
ob.ensureField(fieldOffset + 4)
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetUint64 sets a uint64 field.
func (ob *ObjectBuilder) SetUint64(fieldOffset int, v uint64) {
func (ob ObjectBuilder) SetUint64(fieldOffset int, v uint64) {
ob.ensureField(fieldOffset + 8)
binary.LittleEndian.PutUint64(ob.b.buf[ob.startPos+fieldOffset:], v)
}
// SetInt8 sets an int8 field.
func (ob *ObjectBuilder) SetInt8(fieldOffset int, v int8) {
func (ob ObjectBuilder) SetInt8(fieldOffset int, v int8) {
ob.SetUint8(fieldOffset, uint8(v))
}
// SetInt16 sets an int16 field.
func (ob *ObjectBuilder) SetInt16(fieldOffset int, v int16) {
func (ob ObjectBuilder) SetInt16(fieldOffset int, v int16) {
ob.SetUint16(fieldOffset, uint16(v))
}
// SetInt32 sets an int32 field.
func (ob *ObjectBuilder) SetInt32(fieldOffset int, v int32) {
func (ob ObjectBuilder) SetInt32(fieldOffset int, v int32) {
ob.SetUint32(fieldOffset, uint32(v))
}
// SetInt64 sets an int64 field.
func (ob *ObjectBuilder) SetInt64(fieldOffset int, v int64) {
func (ob ObjectBuilder) SetInt64(fieldOffset int, v int64) {
ob.SetUint64(fieldOffset, uint64(v))
}
// SetFloat32 sets a float32 field.
func (ob *ObjectBuilder) SetFloat32(fieldOffset int, v float32) {
func (ob ObjectBuilder) SetFloat32(fieldOffset int, v float32) {
ob.SetUint32(fieldOffset, float32bits(v))
}
// SetFloat64 sets a float64 field.
func (ob *ObjectBuilder) SetFloat64(fieldOffset int, v float64) {
func (ob ObjectBuilder) SetFloat64(fieldOffset int, v float64) {
ob.SetUint64(fieldOffset, float64bits(v))
}
@@ -191,7 +194,7 @@ func (ob *ObjectBuilder) SetFloat64(fieldOffset int, v float64) {
//
// Panics on len(v) == 0 are avoided: a zero-length argument is a
// no-op (the slot is left as the zero value).
func (ob *ObjectBuilder) SetBytesFixed(fieldOffset int, v []byte) {
func (ob ObjectBuilder) SetBytesFixed(fieldOffset int, v []byte) {
if len(v) == 0 {
return
}
@@ -200,38 +203,35 @@ func (ob *ObjectBuilder) SetBytesFixed(fieldOffset int, v []byte) {
}
// SetText sets a text (string) field.
func (ob *ObjectBuilder) SetText(fieldOffset int, v string) {
func (ob ObjectBuilder) SetText(fieldOffset int, v string) {
ob.SetBytes(fieldOffset, []byte(v))
}
// SetBytes sets a bytes field.
// The data is written after the object's fixed section during Finish().
func (ob *ObjectBuilder) SetBytes(fieldOffset int, v []byte) {
ob.ensureField(fieldOffset + 8) // offset + length
func (ob ObjectBuilder) SetBytes(fieldOffset int, v []byte) {
if len(v) == 0 {
// Null pointer
// Null pointer (offset 0, length 0).
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset:], 0)
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset+4:], 0)
return
}
// The fixed section is fully reserved by StartObject, so b.pos is at the
// tail: append the data now and patch this field's (relOffset, length) pair
// immediately — no deferred offset list, no per-object slice allocation.
dataPos := ob.b.pos
ob.b.grow(len(v))
copy(ob.b.buf[ob.b.pos:ob.b.pos+len(v)], v)
ob.b.pos += len(v)
// Store the data and length; the relative offset is patched in Finish().
// ZERO-COPY: v is retained (not copied) — the caller must keep v valid and
// unmodified until Finish()/FinishAsRoot() returns. Every in-tree builder
// constructs and finishes in one frame, so this is free; it removes one
// alloc+copy per bytes field (the write hot path's dominant cost).
ob.offsets = append(ob.offsets, offsetEntry{
fieldOffset: fieldOffset,
data: v,
})
// Write the length now
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset+4:], uint32(len(v)))
fieldAbsPos := ob.startPos + fieldOffset
relOffset := int32(dataPos - fieldAbsPos)
binary.LittleEndian.PutUint32(ob.b.buf[fieldAbsPos:], uint32(relOffset))
binary.LittleEndian.PutUint32(ob.b.buf[fieldAbsPos+4:], uint32(len(v)))
}
// SetObject sets a nested object field (by offset).
func (ob *ObjectBuilder) SetObject(fieldOffset int, objOffset int) {
func (ob ObjectBuilder) SetObject(fieldOffset int, objOffset int) {
ob.ensureField(fieldOffset + 4)
if objOffset == 0 {
@@ -245,7 +245,7 @@ func (ob *ObjectBuilder) SetObject(fieldOffset int, objOffset int) {
}
// SetList sets a list field.
func (ob *ObjectBuilder) SetList(fieldOffset int, listOffset int, length int) {
func (ob ObjectBuilder) SetList(fieldOffset int, listOffset int, length int) {
ob.ensureField(fieldOffset + 8)
if listOffset == 0 || length == 0 {
@@ -259,7 +259,7 @@ func (ob *ObjectBuilder) SetList(fieldOffset int, listOffset int, length int) {
binary.LittleEndian.PutUint32(ob.b.buf[ob.startPos+fieldOffset+4:], uint32(length))
}
func (ob *ObjectBuilder) ensureField(endOffset int) {
func (ob ObjectBuilder) ensureField(endOffset int) {
needed := ob.startPos + endOffset
if needed > ob.b.pos {
ob.b.grow(needed - ob.b.pos)
@@ -289,39 +289,20 @@ func (ob *ObjectBuilder) ensureField(endOffset int) {
// This is the exported counterpart to the internal ensureField helper.
// It is used by zapv1.WriteList to keep the parent's payload reserved
// before list elements are appended.
func (ob *ObjectBuilder) ReserveFixed(dataSize int) {
func (ob ObjectBuilder) ReserveFixed(dataSize int) {
ob.ensureField(dataSize)
}
// Finish finalizes the object and returns its offset.
// Writes deferred text/bytes data after the object's fixed section
// and patches relative offsets.
func (ob *ObjectBuilder) Finish() int {
// Ensure minimum size for fixed fields
ob.ensureField(ob.dataSize)
// Write deferred text/bytes data and patch relative offsets
for _, entry := range ob.offsets {
if entry.data == nil {
continue
}
// Write the data at current position (after fixed section)
dataPos := ob.b.pos
ob.b.grow(len(entry.data))
copy(ob.b.buf[ob.b.pos:ob.b.pos+len(entry.data)], entry.data)
ob.b.pos += len(entry.data)
// Patch the relative offset: relOffset = dataPos - fieldAbsPos
fieldAbsPos := ob.startPos + entry.fieldOffset
relOffset := int32(dataPos - fieldAbsPos)
binary.LittleEndian.PutUint32(ob.b.buf[fieldAbsPos:], uint32(relOffset))
}
// Finish finalizes the object and returns its offset. Every field (fixed and
// variable) is written eagerly by its Set* call — the fixed section is reserved
// in StartObject and SetBytes/SetText append their tail immediately — so there
// is nothing to replay here.
func (ob ObjectBuilder) Finish() int {
return ob.startPos
}
// FinishAsRoot finalizes and sets as the message root.
func (ob *ObjectBuilder) FinishAsRoot() int {
func (ob ObjectBuilder) FinishAsRoot() int {
offset := ob.Finish()
ob.b.rootOffset = offset
return offset
+8 -8
View File
@@ -11,7 +11,7 @@ import (
// buildSeedMessage builds a valid ZAP message using the Builder for use as
// fuzz seed corpus. Returns the raw bytes.
func buildSeedMessage(fields func(ob *ObjectBuilder)) []byte {
func buildSeedMessage(fields func(ob ObjectBuilder)) []byte {
b := NewBuilder(256)
ob := b.StartObject(64)
fields(ob)
@@ -36,12 +36,12 @@ func buildSeedMessage(fields func(ob *ObjectBuilder)) []byte {
func FuzzParse(f *testing.F) {
// Seed corpus: every valid construction we can think of, plus the
// adversarial buffers that exercised RED-HIGH-1/2/3.
f.Add(buildSeedMessage(func(ob *ObjectBuilder) { ob.SetUint64(0, 0xDEADBEEF) }))
f.Add(buildSeedMessage(func(ob *ObjectBuilder) {
f.Add(buildSeedMessage(func(ob ObjectBuilder) { ob.SetUint64(0, 0xDEADBEEF) }))
f.Add(buildSeedMessage(func(ob ObjectBuilder) {
ob.SetUint32(0, 42)
ob.SetText(4, "round-trip")
}))
f.Add(buildSeedMessage(func(ob *ObjectBuilder) {
f.Add(buildSeedMessage(func(ob ObjectBuilder) {
ob.SetBool(0, true)
ob.SetBytes(4, []byte{0xCA, 0xFE, 0xBA, 0xBE})
}))
@@ -56,7 +56,7 @@ func FuzzParse(f *testing.F) {
f.Add(hdr)
}
// Seed with an empty list (length=0, offset=0).
f.Add(buildSeedMessage(func(ob *ObjectBuilder) {
f.Add(buildSeedMessage(func(ob ObjectBuilder) {
ob.SetList(0, 0, 0)
ob.SetUint64(8, 1)
}))
@@ -123,12 +123,12 @@ func FuzzParse(f *testing.F) {
// produce a valid Message with accessible root.
func FuzzZAPParse(f *testing.F) {
// Seed 1: valid minimal message (uint64 field)
f.Add(buildSeedMessage(func(ob *ObjectBuilder) {
f.Add(buildSeedMessage(func(ob ObjectBuilder) {
ob.SetUint64(0, 0xDEADBEEF)
}))
// Seed 2: valid message with text
f.Add(buildSeedMessage(func(ob *ObjectBuilder) {
f.Add(buildSeedMessage(func(ob ObjectBuilder) {
ob.SetUint32(0, 42)
ob.SetText(4, "hello fuzz")
ob.SetBool(12, true)
@@ -251,7 +251,7 @@ func FuzzZAPRoundtrip(f *testing.F) {
// bytes. Parse must return an error or a valid message -- never panic.
func FuzzZAPMalformedHeader(f *testing.F) {
// Build a real message to use as the base
base := buildSeedMessage(func(ob *ObjectBuilder) {
base := buildSeedMessage(func(ob ObjectBuilder) {
ob.SetUint64(0, 12345)
ob.SetUint64(8, 67890)
})