Documentation
¶
Overview ¶
Package qdf is a compact, streaming-friendly binary serialization format.
Quick start ¶
One encode entry point: Marshal(v, opts). The Options bit-mask picks which codecs run for that call. Convenience bundles cover the common tradeoffs:
OptSpeed — Fast mode, no codecs (matches encoding/json shape)
OptBalanced — Dense + QPack + shape interning + Markov-1 + MTF
OptCompression — OptBalanced + heavier float codecs: Gorilla XOR for
smooth time-series and ALP for quantized/decimal
float64 (large wire reduction at higher encode CPU)
A single decoder handles every variant; the wire header self- describes the dialect.
b, err := qdf.Marshal(v, qdf.OptSpeed) // hot path b, err := qdf.Marshal(v, qdf.OptBalanced) // telemetry / logs b, err := qdf.Marshal(v, qdf.OptCompression) // backup / archive err := qdf.Unmarshal(b, &v) enc := qdf.NewStreamEncoder(w, qdf.OptBalanced) dec := qdf.NewStreamDecoder(r)
Marshal returns a freshly-allocated slice owned by the caller. AppendMarshal is the zero-extra-copy variant.
Data shapes and codecs ¶
Under OptBalanced the encoder picks a codec per slice from the data itself: integer slices use Frame-of-Reference, delta, run-length or dictionary coding; bool slices bit-pack; repeated strings are interned and back-referenced. A slice of homogeneous flat structs is transposed and compressed column-by-column automatically — numeric columns get the integer codecs, repeated string columns collapse — with no flag, and a per-array probe falls back to the plain row encoding when columnar would not help. The float codecs that trade encode CPU for size live behind OptCompression: Gorilla XOR on smooth series, ALP on quantized/decimal float64 grids (quantized telemetry, prices, latencies).
Concurrency ¶
Marshal, Unmarshal and AppendMarshal are safe for concurrent use: each call leases its own encoder/decoder from a pool. A single Encoder, Decoder or StreamEncoder value is not safe to share across goroutines — use one per goroutine. A Dense document is a sequential stream, so one document cannot be encoded or decoded in parallel; to parallelize a large dataset, split it into independent shards and Marshal each separately.
See docs/USAGE.md in the repository for a fuller guide.
Public API surface ¶
The package contract is split across four layers; everything else under internal/ is implementation detail and may change between releases without notice.
Top-level entry points (file: qdf.go):
func Marshal(v any, opts Options) ([]byte, error) func AppendMarshal(dst []byte, v any, opts Options) ([]byte, error) func Unmarshal(data []byte, out any) error type Options uint32 // bit-mask of OptDense, OptQPack, OptMTF, …
Typed convenience wrappers — generic, zero-extra-reflection (file: qdf_generic.go):
func MarshalT[T any](v T, opts Options) ([]byte, error) func AppendMarshalT[T any](dst []byte, v T, opts Options) ([]byte, error) func UnmarshalT[T any](data []byte) (T, error)
Low-level encoder / decoder for callers driving the wire directly or interoperating with the qdfgen code generator (files: encoder.go, decoder.go, stream.go):
type Encoder struct{ … }
NewEncoder(mode Mode) *Encoder
NewEncoderWith(opts Options) *Encoder
NewEncoderOnBuf(buf []byte, mode Mode) *Encoder
(e *Encoder) WriteString / WriteBytes / WriteInt / WriteUint /
WriteFloat32 / WriteFloat64 / WriteBool / WriteNil /
WriteArrayHeader / WriteMapHeader /
WriteTimestampNano / WriteStringInline
(e *Encoder) AppendBytes / EnsureHeader / Bytes / Take /
Reset / SetBuffer / AdoptBuffer / SetIntern /
SetMaxDepth / SetQPack / QPack / ApplyOpts /
EncodeValue / PreIntern
type Decoder struct{ … }
NewDecoder() *Decoder
NewDecoderOnBuf(buf []byte) *Decoder
(d *Decoder) ReadString / ReadStringBytes / ReadBytes /
ReadBool / ReadInt / ReadUint / ReadFloat32 /
ReadFloat64 / ReadNil / ReadArrayHeader /
ReadMapHeader / ReadTimestampNano / Skip /
PeekTag / IsNil / Pos / Remaining / RemainingBytes /
Advance / SetInput / SetNoCopy / MarkHeaderRead /
CheckLength / InternKey
type StreamEncoder, type StreamDecoder // io.Writer / io.Reader
User-side hook points (file: marshaler.go):
type Marshaler interface { MarshalQDF(dst []byte) ([]byte, error) }
type Unmarshaler interface { UnmarshalQDF(src []byte) (int, error) }
The qdfgen code generator (cmd/qdfgen) emits MarshalQDF / UnmarshalQDF methods for user struct types; the codegen path uses the same Encoder / Decoder primitives listed above and requires no reflection at runtime.
Example ¶
Example demonstrates the simplest possible round-trip: Marshal a value into a freshly-allocated byte slice and Unmarshal it back into a typed receiver.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Event struct {
ID int `qdf:"id"`
Source string `qdf:"source"`
}
in := Event{ID: 42, Source: "ingest"}
buf, err := qdf.Marshal(in, qdf.OptSpeed)
if err != nil {
panic(err)
}
var out Event
if err := qdf.Unmarshal(buf, &out); err != nil {
panic(err)
}
fmt.Printf("%+v\n", out)
}
Output: {ID:42 Source:ingest}
Example (RoundTripSlice) ¶
Example_roundTripSlice demonstrates encoding a slice of structs. QDF's reflect path resolves the element type once per typeDesc; every element after the first emits its tag stream straight from the cached descriptor.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Row struct {
Service string `qdf:"service"`
Status int `qdf:"status"`
}
rows := []Row{
{"auth", 200},
{"billing", 503},
{"auth", 200},
}
buf, _ := qdf.Marshal(rows, qdf.OptBalanced)
var got []Row
_ = qdf.Unmarshal(buf, &got)
for _, r := range got {
fmt.Printf("%s=%d\n", r.Service, r.Status)
}
}
Output: auth=200 billing=503 auth=200
Index ¶
- Constants
- Variables
- func AppendMarshal(dst []byte, v any, opts Options) ([]byte, error)
- func AppendMarshalDirect[T Marshaler](dst []byte, v T) ([]byte, error)
- func AppendMarshalT[T any](dst []byte, v T, opts Options) ([]byte, error)
- func Marshal(v any, opts Options) ([]byte, error)
- func MarshalDirect[T Marshaler](v T) ([]byte, error)
- func MarshalT[T any](v T, opts Options) ([]byte, error)
- func Unmarshal(data []byte, out any) error
- func UnmarshalDirect[T Unmarshaler](data []byte, out T) error
- func UnmarshalT[T any](data []byte, out *T) error
- type Decoder
- func (d *Decoder) Advance(n int)
- func (d *Decoder) CheckLength(n int, perElem int) error
- func (d *Decoder) InternKey(b []byte) string
- func (d *Decoder) IsNil() (bool, error)
- func (d *Decoder) MarkHeaderRead()
- func (d *Decoder) PeekTag() (byte, error)
- func (d *Decoder) Pos() int
- func (d *Decoder) ReadArrayHeader() (int, error)
- func (d *Decoder) ReadBool() (bool, error)
- func (d *Decoder) ReadBytes() ([]byte, error)
- func (d *Decoder) ReadFloat32() (float32, error)
- func (d *Decoder) ReadFloat64() (float64, error)
- func (d *Decoder) ReadInt() (int64, error)
- func (d *Decoder) ReadMapHeader() (int, error)
- func (d *Decoder) ReadNil() error
- func (d *Decoder) ReadString() (string, error)
- func (d *Decoder) ReadStringBytes() ([]byte, error)
- func (d *Decoder) ReadTimestampNano() (int64, error)
- func (d *Decoder) ReadUint() (uint64, error)
- func (d *Decoder) Remaining() int
- func (d *Decoder) RemainingBytes() []byte
- func (d *Decoder) SetInput(buf []byte)
- func (d *Decoder) SetNoCopy(v bool)
- func (d *Decoder) Skip() error
- type Encoder
- func (e *Encoder) AdoptBuffer(b []byte)
- func (e *Encoder) AppendBytes(p []byte)
- func (e *Encoder) ApplyOpts(opts Options)
- func (e *Encoder) Bytes() []byte
- func (e *Encoder) EncodeValue(v any) error
- func (e *Encoder) EnsureHeader()
- func (e *Encoder) MarkHeaderWritten()
- func (e *Encoder) PreIntern(strs ...string)
- func (e *Encoder) QPack() bool
- func (e *Encoder) Reset()
- func (e *Encoder) SetBuffer(b []byte)
- func (e *Encoder) SetIntern(min int, cap int)
- func (e *Encoder) SetMaxDepth(d int)
- func (e *Encoder) SetQPack(v bool)
- func (e *Encoder) Take() []byte
- func (e *Encoder) WriteArrayHeader(n int)
- func (e *Encoder) WriteBool(v bool)
- func (e *Encoder) WriteBytes(b []byte)
- func (e *Encoder) WriteFloat32(v float32)
- func (e *Encoder) WriteFloat64(v float64)
- func (e *Encoder) WriteInt(v int64)
- func (e *Encoder) WriteMapHeader(n int)
- func (e *Encoder) WriteNil()
- func (e *Encoder) WriteString(s string)
- func (e *Encoder) WriteStringInline(s string)
- func (e *Encoder) WriteTimestampNano(ns int64)
- func (e *Encoder) WriteUint(v uint64)
- type Marshaler
- type Mode
- type Options
- type StreamDecoder
- type StreamEncoder
- type Unmarshaler
Examples ¶
Constants ¶
const ( Magic0 byte = 'Q' Magic1 byte = 'D' Magic2 byte = 'F' Version1 byte = 0x01 )
const ( FlagDense byte = 1 << 0 // FlagQPack signals that the encoder may emit QPack codec tags // (0xE3..0xEF). A decoder that does not recognize the tags will fail // with ErrBadTag on the first packed slice; the flag is an early hint // so callers can refuse the buffer up front. FlagQPack byte = 1 << 1 // FlagRANS signals that the body (everything after the 5-byte header) is // rANS-compressed: varuint(origLen) + 256-entry frequency table + // rANS stream. The decoder reverses it before reading tags. Set only // when the rANS form is strictly smaller than the plain body. FlagRANS byte = 1 << 2 )
Flag bits in the 5th header byte.
const DefaultMaxDepth = 10_000
DefaultMaxDepth caps reflect-path pointer/struct recursion. Set large enough for any legitimate payload (10 000) while still rejecting genuine cycles before the goroutine stack overflows.
const TagNil = tagNil
TagNil exposes the nil-value tag for comparison with PeekTag.
Variables ¶
var ( ErrShortBuffer = errors.New("qdf: short buffer") ErrBadMagic = errors.New("qdf: bad magic / not a qdf stream") ErrBadVersion = errors.New("qdf: unsupported wire version") ErrBadTag = errors.New("qdf: unknown tag") ErrTypeMismatch = errors.New("qdf: type mismatch on decode") ErrInvalidLength = errors.New("qdf: invalid length prefix") ErrUnknownStateID = errors.New("qdf: unknown state-table id") ErrUnsupported = errors.New("qdf: unsupported type") ErrCycleDetected = errors.New("qdf: pointer cycle detected (max depth exceeded)") )
Functions ¶
func AppendMarshal ¶
AppendMarshal encodes v and appends the result to dst. Reuse the returned slice as dst on the next call to avoid per-message allocations.
Example ¶
ExampleAppendMarshal shows the zero-extra-copy form. The caller keeps a reusable byte buffer and asks Marshal to append into it, returning the extended slice. Combined with a sync.Pool of buffers this drives encode allocations to zero on the hot path.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Tick struct {
Ts int64 `qdf:"ts"`
Price float64 `qdf:"price"`
}
buf := make([]byte, 0, 64) // caller-owned, reusable
t := Tick{Ts: 1_700_000_000, Price: 99.5}
var err error
buf, err = qdf.AppendMarshal(buf, t, qdf.OptSpeed)
if err != nil {
panic(err)
}
var out Tick
if err := qdf.Unmarshal(buf, &out); err != nil {
panic(err)
}
fmt.Printf("%+v\n", out)
}
Output: {Ts:1700000000 Price:99.5}
func AppendMarshalDirect ¶
AppendMarshalDirect appends the serialisation of v to dst.
func AppendMarshalT ¶
AppendMarshalT is the generic equivalent of AppendMarshal.
func Marshal ¶
Marshal encodes v with the given option bit-mask. Combine bits with | to opt into individual codecs, or use one of the OptSpeed / OptBalanced / OptCompression bundles.
b, _ := qdf.Marshal(event, qdf.OptSpeed) // hot path
b, _ := qdf.Marshal(batch, qdf.OptBalanced) // telemetry
b, _ := qdf.Marshal(snapshot, qdf.OptCompression) // backup
b, _ := qdf.Marshal(payload, // tuned
qdf.OptDense|qdf.OptQPack|qdf.OptShapeIntern)
func MarshalDirect ¶
MarshalDirect serialises a value through its MarshalQDF method. Skips the public Marshal entry point's any-boxing, reflect.New, and descriptor lookup. Roughly 2-4× faster than Marshal for generated types on small payloads, with one fewer allocation per call.
func MarshalT ¶
MarshalT is the generic equivalent of Marshal. T is fixed at the call site; opts copies by value, so the call adds zero heap allocations over MarshalT itself.
Example ¶
ExampleMarshalT shows the generic typed wrapper. T is fixed at the call site, so the compiler skips the reflect.TypeOf step that Marshal(v any, opts) pays inside; encode/decode of value types stays on the stack.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Point struct {
X int32 `qdf:"x"`
Y int32 `qdf:"y"`
}
buf, err := qdf.MarshalT(Point{X: 3, Y: 4}, qdf.OptSpeed)
if err != nil {
panic(err)
}
var p Point
if err := qdf.UnmarshalT(buf, &p); err != nil {
panic(err)
}
fmt.Printf("%+v\n", p)
}
Output: {X:3 Y:4}
func Unmarshal ¶
Unmarshal decodes data into out, which must be a non-nil pointer. The wire dialect is detected from the header — the same Unmarshal reads any output produced by Marshal regardless of the encode-side option bits.
func UnmarshalDirect ¶
func UnmarshalDirect[T Unmarshaler](data []byte, out T) error
UnmarshalDirect dispatches to out.UnmarshalQDF after validating the 5-byte header. It is the inverse of MarshalDirect; the same wire constraints apply (Fast-mode only).
If the input carries the FlagDense bit, UnmarshalDirect falls back to the full Unmarshal path because generated code cannot resolve state-ref tags without the decoder's intern table.
Decode-side performance is bounded by the user's UnmarshalQDF implementation: the reflect path is heavily pooled (Decoder pool + per-decoder key intern cache) and tends to win against naive hand-rolled receivers. Generated code from cmd/qdfgen uses Decoder.InternKey for map / struct keys and matches or beats the reflect path; ad-hoc UnmarshalQDF methods that call NewDecoderOnBuf + ReadString in a tight loop will not.
func UnmarshalT ¶
UnmarshalT is the generic equivalent of Unmarshal. The destination pointer must not be nil.
Types ¶
type Decoder ¶
type Decoder struct {
// contains filtered or unexported fields
}
Decoder reads QDF wire data from a single input buffer. Call SetInput to bind a buffer and the typed Read* methods to walk it. Behaviour is undefined if the input is mutated while the decoder holds it.
func NewDecoder ¶
func NewDecoder() *Decoder
NewDecoder constructs a Decoder. Bind input via SetInput.
func NewDecoderOnBuf ¶
NewDecoderOnBuf binds a Decoder to buf. Equivalent to NewDecoder followed by SetInput(buf), without the SetInput branch checks.
func (*Decoder) CheckLength ¶
CheckLength returns ErrShortBuffer when n claimed elements cannot fit in the remaining input. Use before allocating per-element storage.
func (*Decoder) InternKey ¶
InternKey returns a string equal to b, sharing storage with prior identical keys when the cache has them.
func (*Decoder) IsNil ¶
IsNil reports whether the next value is the nil tag, consuming it on true. Returns (false, nil) for any other tag.
Example ¶
ExampleDecoder_IsNil consumes a nil tag if present, returning false (without advancing) for any other tag. Useful when a field is optional on the wire and the caller wants to keep the read cursor stable on a non-nil branch.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
buf, _ := qdf.Marshal([]any{nil, "hello"}, qdf.OptSpeed)
dec := qdf.NewDecoderOnBuf(buf)
n, _ := dec.ReadArrayHeader()
for range n {
isNil, _ := dec.IsNil()
if isNil {
fmt.Println("<nil>")
continue
}
s, _ := dec.ReadString()
fmt.Println(s)
}
}
Output: <nil> hello
func (*Decoder) MarkHeaderRead ¶
func (d *Decoder) MarkHeaderRead()
MarkHeaderRead tells the decoder the magic+version header is already consumed (e.g. the buffer is a tail slice from a parent decoder). The next read will skip the header check.
func (*Decoder) PeekTag ¶
PeekTag returns the next tag byte without advancing the cursor.
Example ¶
ExampleDecoder_PeekTag inspects the next wire tag without consuming it. Handy when implementing a dispatch loop over a schemaless payload, or when an Unmarshaler needs to branch on the upcoming type.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
buf, _ := qdf.Marshal(int64(-42), qdf.OptSpeed)
dec := qdf.NewDecoderOnBuf(buf)
tag, _ := dec.PeekTag()
if tag == qdf.TagNil {
fmt.Println("nil value")
return
}
v, _ := dec.ReadInt()
fmt.Printf("int=%d (tag=0x%02x)\n", v, tag)
}
Output: int=-42 (tag=0xc7)
func (*Decoder) ReadArrayHeader ¶
ReadArrayHeader returns the element count.
func (*Decoder) ReadBytes ¶
ReadBytes returns a []byte value. With noCopy = true the result aliases the input.
func (*Decoder) ReadFloat32 ¶
func (*Decoder) ReadFloat64 ¶
func (*Decoder) ReadMapHeader ¶
ReadMapHeader returns the key/value pair count.
func (*Decoder) ReadString ¶
ReadString returns the next string. If the decoder is in noCopy mode the returned string aliases the input buffer; otherwise a copy is made.
When the tag at the cursor resolves through the intern table — tagInternStr, tagStateRef, tagStateMTF, tagStatePair, tagStateRepeat — readStringBytes leaves d.state.lastID set to the entry it just touched. We return the pre-materialised d.state.stringValues[id] from that path instead of allocating a fresh `string(b)` copy on every state-ref hit. Inline reads (fixstr, str8/16/32) set lastID = lruInvalidID, fall through, and pay the copy as before.
func (*Decoder) ReadStringBytes ¶
ReadStringBytes returns the next string or []byte value without copying. The returned slice aliases either the input buffer or the decoder's state-table storage. Callers that retain it beyond the input's lifetime must copy.
func (*Decoder) ReadTimestampNano ¶
func (*Decoder) ReadUint ¶
ReadUint reads a value that was encoded with WriteUint or WriteInt with a non-negative argument. Returns an error if the next value is negative.
func (*Decoder) RemainingBytes ¶
RemainingBytes returns the unread portion of the input buffer. The result aliases the buffer and is invalidated by the next SetInput.
func (*Decoder) SetNoCopy ¶
SetNoCopy switches the decoder into aliasing mode: string and []byte reads return slices that share storage with the input buffer. Faster, but the caller must not retain the result past the lifetime of the input.
Example ¶
ExampleDecoder_SetNoCopy returns string / []byte values that alias the input buffer instead of allocating per-value copies. Use this for read-heavy hot paths where the input buffer outlives every value the decoder hands back; do not retain the values past the buffer's lifetime.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
in := []string{"alpha", "beta", "gamma"}
buf, _ := qdf.Marshal(in, qdf.OptSpeed)
dec := qdf.NewDecoderOnBuf(buf)
dec.SetNoCopy(true)
n, _ := dec.ReadArrayHeader()
for range n {
s, _ := dec.ReadString() // alias into buf
fmt.Println(s)
}
}
Output: alpha beta gamma
type Encoder ¶
type Encoder struct {
// contains filtered or unexported fields
}
Encoder writes a single QDF value into a growing internal buffer. Reset drops the buffer contents and (in Dense mode) the intern table so the encoder can be reused.
Example ¶
ExampleEncoder drives the encoder by hand instead of via the top-level Marshal pool. Use this when you need to:
- keep a reusable buffer alive across many encodes;
- call PreIntern with a known hot string pool;
- mix WriteX primitives with reflect-driven EncodeValue.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
enc := qdf.NewEncoderWith(qdf.OptSpeed)
// Primitives + struct fields can be mixed by hand.
enc.WriteMapHeader(2)
enc.WriteString("name")
enc.WriteString("alice")
enc.WriteString("age")
enc.WriteInt(33)
dec := qdf.NewDecoderOnBuf(enc.Bytes())
n, _ := dec.ReadMapHeader()
for range n {
k, _ := dec.ReadString()
switch k {
case "name":
v, _ := dec.ReadString()
fmt.Printf("%s=%s\n", k, v)
case "age":
v, _ := dec.ReadInt()
fmt.Printf("%s=%d\n", k, v)
}
}
}
Output: name=alice age=33
func NewEncoder ¶
NewEncoder returns an Encoder. The internal buffer is allocated lazily on first write.
func NewEncoderOnBuf ¶
NewEncoderOnBuf returns an Encoder that appends to buf at its current length. The buffer is NOT truncated; pass an empty slice for a fresh encoding. Call SetBuffer afterwards to truncate.
func NewEncoderWith ¶
NewEncoderWith returns an Encoder configured by the option bit-mask directly. Dense state machinery is allocated only when OptDense is set. Defaults for intern threshold / depth match NewEncoder.
func (*Encoder) AdoptBuffer ¶
AdoptBuffer installs b as the working buffer, preserving its current length. Used to continue writing into a buffer that already carries data — for example, after a nested type returned its extended slice. headerOut is left unchanged.
func (*Encoder) AppendBytes ¶
AppendBytes appends raw, already-valid wire bytes. Bypasses tag dispatch; used by generated code to emit pre-encoded field-name prefixes.
func (*Encoder) ApplyOpts ¶
ApplyOpts reconfigures the encoder via the options bit-mask. Equivalent to recreating the encoder with NewEncoderWith but preserves the existing pool buffer and (Dense-mode) intern state. The pool-backed Marshal* entry points call this on every acquire; callers driving an Encoder directly use it to switch between OptSpeed and OptBalanced without re-allocating.
func (*Encoder) Bytes ¶
Bytes returns the encoded payload. It aliases the encoder's buffer and is only valid until the next write or Reset.
func (*Encoder) EncodeValue ¶
EncodeValue runs the reflect-driven Marshal pipeline on v against this Encoder. Convenience for callers driving an Encoder directly (e.g. with PreIntern) — equivalent to what the pool-backed Marshal does internally.
Example ¶
ExampleEncoder_EncodeValue drives the reflect-based encode path against any Go value directly through the encoder, bypassing the pool used by qdf.Marshal. Combine with PreIntern, custom SetBuffer, or SetIntern tuning for a long-lived encoder that shapes its state for a specific workload.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Config struct {
Name string `qdf:"name"`
Limit int `qdf:"limit"`
}
enc := qdf.NewEncoderWith(qdf.OptBalanced)
if err := enc.EncodeValue(Config{Name: "service.cpu", Limit: 1000}); err != nil {
panic(err)
}
var out Config
_ = qdf.Unmarshal(enc.Bytes(), &out)
fmt.Printf("%+v\n", out)
}
Output: {Name:service.cpu Limit:1000}
func (*Encoder) EnsureHeader ¶
func (e *Encoder) EnsureHeader()
EnsureHeader forces a header write if one has not been emitted yet.
func (*Encoder) MarkHeaderWritten ¶
func (e *Encoder) MarkHeaderWritten()
MarkHeaderWritten tells the encoder the QDF header is already present in its buffer (e.g. left there by a parent encoder). The next write will skip the header emission.
func (*Encoder) PreIntern ¶
PreIntern registers the given strings against the encoder's intern table up front. Subsequent WriteString / WriteBytes calls that pass the SAME backing string header (i.e. the same underlying byte pointer and length) skip the hash + slot probe and emit a state-ref directly against the cached intern id.
Intended for power users who know their hot string pool ahead of time — service names, region codes, enum-like values drawn from a fixed slice. Real telemetry payloads draw 90 %+ of their dense intern hits from such pools. Skipping the hash/probe on those calls is the main per-emit cost left on the encode hot path.
Requires OptDense to be applied first (otherwise the call is a no-op). The registered identities are dropped on Reset so a pooled encoder does not carry caller-supplied pointers across recycles.
Safety: the caller must keep the backing memory of every PreIntern'd string alive for the lifetime of the next encode call. For literals embedded in a slice / global / struct field this is automatic; for short-lived stack strings the caller is responsible.
Example ¶
ExampleEncoder_PreIntern registers a known string pool against the encoder so subsequent WriteString calls with the same backing pointer skip the intern table's hash + slot probe and emit a state-ref directly. Use this when the caller knows the hot vocabulary up front (service names, region codes, enum- like fields).
The caller must keep the registered strings' backing memory alive for the lifetime of the next encode call. String literals embedded in a slice, global, or struct field stay alive automatically; short-lived stack strings do not.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Row struct {
Service string `qdf:"service"`
Region string `qdf:"region"`
Status int `qdf:"status"`
}
// Stable hot pool — backing slices live for the program's
// lifetime, so the registered pointers stay valid across
// many encode calls on the same encoder.
services := []string{"billing", "auth", "ingest"}
regions := []string{"eu-west-1", "us-east-1"}
enc := qdf.NewEncoderWith(qdf.OptBalanced)
enc.PreIntern(services...)
enc.PreIntern(regions...)
rows := []Row{
{services[0], regions[0], 200},
{services[1], regions[1], 500},
{services[2], regions[0], 200},
}
if err := enc.EncodeValue(rows); err != nil {
panic(err)
}
var out []Row
_ = qdf.Unmarshal(enc.Bytes(), &out)
for _, r := range out {
fmt.Printf("%s/%s=%d\n", r.Service, r.Region, r.Status)
}
}
Output: billing/eu-west-1=200 auth/us-east-1=500 ingest/eu-west-1=200
func (*Encoder) Reset ¶
func (e *Encoder) Reset()
Reset truncates the buffer and resets the intern table. Capacities are preserved. opts / mode / qpack are also reset to OptSpeed so a pooled encoder does not leak its previous configuration into the next caller — apply the desired options explicitly via applyOpts / SetQPack after Reset.
func (*Encoder) SetIntern ¶
SetIntern overrides the Dense-mode tuning knobs. Zero values keep the current setting.
func (*Encoder) SetMaxDepth ¶
SetMaxDepth caps reflect-path pointer/struct recursion. The default (DefaultMaxDepth = 10000) is sufficient for any normal payload and rejects pointer cycles before they stack-overflow the goroutine. Set to 0 to disable the check entirely — only safe when the caller can prove the input graph is acyclic.
func (*Encoder) SetQPack ¶
SetQPack toggles QPack codec emission. When true, slice fast paths produce packed/encoded forms (bitpacked bools, FOR-packed integers, Gorilla-encoded floats, raw-LE bulk) instead of per-element tag streams. Setting must happen before the first write of the value (the header is emitted lazily and carries the FlagQPack hint when this is on).
func (*Encoder) Take ¶
Take returns the encoded payload and detaches it from the encoder. The caller takes ownership.
func (*Encoder) WriteArrayHeader ¶
WriteArrayHeader writes the header for an array of n elements. The caller must follow with exactly n element writes.
func (*Encoder) WriteBytes ¶
WriteBytes writes a []byte. In Dense mode, eligible payloads are intern-encoded. The intern table is keyed on the byte sequence, so a string and a []byte with identical content share an ID.
func (*Encoder) WriteFloat32 ¶
func (*Encoder) WriteFloat64 ¶
func (*Encoder) WriteMapHeader ¶
WriteMapHeader writes the header for a map of n entries. The caller must follow with exactly n key/value pairs.
func (*Encoder) WriteString ¶
WriteString writes s. In Dense mode (OptDense set), eligible strings are intern-encoded.
func (*Encoder) WriteStringInline ¶
WriteStringInline forces an in-line encoding even when Dense intern would be eligible. Use for fields known to be unique per message.
func (*Encoder) WriteTimestampNano ¶
WriteTimestampNano writes a Unix-nanoseconds timestamp.
type Marshaler ¶
Marshaler is implemented by types that know how to serialize themselves into the QDF wire format. Implementations should append exactly one value to dst and return the extended slice.
Example ¶
ExampleMarshaler shows a custom MarshalQDF / UnmarshalQDF pair. Use the interface when you want a compact wire form for a type — e.g. packing a small struct into a single primitive, versioning a payload, or pre-validating bytes before they hit the wire.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
// Color is a 24-bit RGB value stored as a single uint32 on the
// wire. It implements Marshaler / Unmarshaler so qdf encodes it
// as a primitive integer instead of recursing into reflect.
type Color struct{ R, G, B uint8 }
// MarshalQDF packs the three bytes into a uint32 and writes it
// using the encoder primitives from a temporary qdf.Encoder
// over the caller's buffer. This shape — temporary encoder over
// the caller's dst slice — is what qdfgen produces by default.
func (c Color) MarshalQDF(dst []byte) ([]byte, error) {
enc := qdf.NewEncoderOnBuf(dst, qdf.Fast)
enc.MarkHeaderWritten() // dst already contains the parent header
enc.WriteUint(uint64(c.R)<<16 | uint64(c.G)<<8 | uint64(c.B))
return enc.Bytes(), nil
}
// UnmarshalQDF reverses the pack and returns the number of bytes
// consumed from src.
func (c *Color) UnmarshalQDF(src []byte) (int, error) {
dec := qdf.NewDecoderOnBuf(src)
dec.MarkHeaderRead()
v, err := dec.ReadUint()
if err != nil {
return 0, err
}
c.R = uint8(v >> 16)
c.G = uint8(v >> 8)
c.B = uint8(v)
return dec.Pos(), nil
}
// ExampleMarshaler shows a custom MarshalQDF / UnmarshalQDF pair.
// Use the interface when you want a compact wire form for a
// type — e.g. packing a small struct into a single primitive,
// versioning a payload, or pre-validating bytes before they hit
// the wire.
func main() {
in := Color{R: 0x12, G: 0x34, B: 0x56}
buf, _ := qdf.MarshalDirect(&in)
var out Color
_ = qdf.UnmarshalDirect(buf, &out)
fmt.Printf("R=%02x G=%02x B=%02x\n", out.R, out.G, out.B)
}
Output: R=12 G=34 B=56
type Options ¶
type Options uint32
Options is a bit-mask of per-call encoder feature toggles. A zero value (OptSpeed) gives the fastest path: Fast mode, no codecs, no predictors. Or-combine bits with | to opt in to individual codecs. Pre-built bundles cover the common "max speed", "balanced", and "max compression" use cases without having to remember the layout.
Options carry by value (uint32) so Marshal / AppendMarshal add zero allocations over the pool / output-clone overhead. The encoder checks each bit with a single AND on the hot path.
Example ¶
ExampleOptions shows how the Options bit-mask picks which codec layers run for a Marshal call. The same input encodes to a progressively smaller wire as more codecs opt in, with a small CPU penalty per added codec. The decoder reads any dialect off the wire header so changing Options is a producer-side decision.
package main
import (
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Row struct {
Service string `qdf:"service"`
Region string `qdf:"region"`
Status int `qdf:"status"`
}
// Repetitive batch — every row shares the same service and region.
in := make([]Row, 50)
for i := range in {
in[i] = Row{Service: "billing", Region: "eu-west-1", Status: 200}
}
for _, p := range []struct {
name string
opts qdf.Options
}{
{"OptSpeed", qdf.OptSpeed},
{"OptBalanced", qdf.OptBalanced},
} {
buf, _ := qdf.Marshal(in, p.opts)
fmt.Printf("%-12s wire=%d bytes\n", p.name, len(buf))
}
}
Output: OptSpeed wire=2208 bytes OptBalanced wire=161 bytes
const ( // OptDense activates the inline intern table. First occurrences of // strings and []byte payloads are stored once and back-referenced // by ID (1-3 bytes per reuse). Required for any of the state-ref // codecs (Repeat / MTF / Pair) and for tagMapShape. OptDense Options = 1 << iota // OptQPack enables the numeric / boolean slice codecs. Bools // bit-pack; integer slices try Frame-of-Reference and Delta+FOR; // float slices stay on raw-LE. Auto-selected per slice; the // encoder falls back to raw-LE when nothing wins. Gorilla XOR // for floats lives behind OptGorillaFloat (bit 5) — see // OptCompression for the bundle that turns it on. OptQPack // OptShapeIntern routes struct emissions through tagMapShape: each // distinct struct type declares its field ordering on first emit; // subsequent emits write only the shape ID + values. Requires // OptDense (the shape table lives on the intern table side). OptShapeIntern // OptPairPred enables the Markov-1 successor predictor // (tagStatePair, 0xEA). Per previous state-ref ID the encoder keeps // a ring of the last 4 successors; a hit emits the ring rank in a // single byte. Requires OptDense. OptPairPred // OptMTF enables Move-to-Front rank coding (tagStateMTF, 0xE9). // When the LRU rank of a state-ref ID needs fewer varuint bytes // than the raw id, the rank is emitted instead. Requires OptDense. OptMTF // OptGorillaFloat opts in to the Gorilla XOR codec for []float64 // (and []float32) slices. Gorilla collapses smooth time-series // data dramatically (~75 % wire reduction on quantised metric // streams) but trades that for ~10× more CPU on the encode/decode // path because the body is bit-level. Off by default in // OptBalanced for that reason; included in OptCompression for // archive-style workloads where wire size dominates. Requires // OptQPack. OptGorillaFloat // OptRANS adds a final static order-0 rANS entropy pass over the whole // encoded body. Opt-in (bundled into OptCompression) because it trades // encode/decode CPU for wire size. The encoder applies it only when the // rANS form is strictly smaller than the plain body, so it never makes a // buffer larger. Whole-buffer, so the streaming encoder ignores it. OptRANS // OptSpeed is the zero-bit preset: Fast mode, no codecs, no // predictors. Maximum throughput, smallest CPU footprint. OptSpeed Options = 0 // OptBalanced bundles every codec that does not trade CPU for // compression beyond its sweet spot. The right default for // telemetry, log batches, and any payload with repetitive // strings or numeric slices. Notably excludes OptGorillaFloat — // reach for OptCompression when the float slices in the payload // are smooth time-series and wire size matters more than encode // latency. OptBalanced Options = OptDense | OptQPack | OptShapeIntern | OptPairPred | OptMTF // OptCompression bundles every codec that the encoder will spend // CPU on for wire-size gains. On top of OptBalanced it adds // OptGorillaFloat (Gorilla XOR for float slices), which trades // encode/decode CPU for wire size, so it stays out of the // OptBalanced default; future heavy codecs (rANS, dictionary // preloading) will land in this bundle without breaking the name. OptCompression Options = OptBalanced | OptGorillaFloat | OptRANS )
type StreamDecoder ¶
type StreamDecoder struct {
// contains filtered or unexported fields
}
StreamDecoder reads a sequence of values from an io.Reader. The intern table is preserved across Decode calls to match StreamEncoder.
The decoder grows its window buffer as needed. State-table entries alias the buffer, so the window is never compacted during a stream; total memory tracks the stream length. For unbounded streams partition into envelopes and create a fresh StreamDecoder per envelope.
func NewStreamDecoder ¶
func NewStreamDecoder(r io.Reader) *StreamDecoder
NewStreamDecoder returns a stream decoder reading from r.
func (*StreamDecoder) Close ¶
func (s *StreamDecoder) Close() error
Close releases the scratch buffer to the pool. The underlying reader is not closed. Idempotent: subsequent calls are a safe no-op.
func (*StreamDecoder) Decode ¶
func (s *StreamDecoder) Decode(out any) error
Decode reads the next value into out. out must be a pointer.
func (*StreamDecoder) SetNoCopy ¶
func (s *StreamDecoder) SetNoCopy(v bool)
SetNoCopy mirrors Decoder.SetNoCopy.
Example ¶
ExampleStreamDecoder_SetNoCopy decodes a multi-message stream without copying the decoded strings out of the input buffer. The decoded values alias the io.Reader's payload; the caller must not retain the values past the next Decode call.
package main
import (
"bytes"
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Event struct {
Service string `qdf:"service"`
}
// Build a 2-message stream.
var sink bytes.Buffer
enc := qdf.NewStreamEncoderWith(&sink, qdf.OptBalanced)
_ = enc.Encode(Event{Service: "auth"})
_ = enc.Encode(Event{Service: "auth"})
_ = enc.Flush()
_ = enc.Close()
dec := qdf.NewStreamDecoder(&sink)
dec.SetNoCopy(true) // aliased reads — zero allocations
defer dec.Close() //nolint:errcheck // example
var ev Event
for dec.Decode(&ev) == nil {
fmt.Println(ev.Service)
}
}
Output: auth auth
type StreamEncoder ¶
type StreamEncoder struct {
// contains filtered or unexported fields
}
StreamEncoder writes a sequence of values into an io.Writer. The header is emitted once before the first value; in Dense mode the intern table survives across Encode calls so back-references span the whole stream.
Example ¶
ExampleStreamEncoder writes a batch of messages into a single io.Writer where the Dense-mode intern table, shape table, and predictors survive across calls. The first message of every shape pays for the key intern records; every subsequent message of the same shape emits values only.
This is the form to reach for when piping events to a file or a network socket — per-message Marshal would reset the state table each call and lose the cross-message compression.
package main
import (
"bytes"
"fmt"
"github.com/alex60217101990/qdf"
)
func main() {
type Event struct {
Service string `qdf:"service"`
Status int `qdf:"status"`
}
var sink bytes.Buffer
enc := qdf.NewStreamEncoderWith(&sink, qdf.OptBalanced)
defer enc.Close() //nolint:errcheck // example
events := []Event{
{"billing", 200},
{"billing", 200},
{"billing", 500},
}
for _, e := range events {
if err := enc.Encode(e); err != nil {
panic(err)
}
}
if err := enc.Flush(); err != nil {
panic(err)
}
dec := qdf.NewStreamDecoder(&sink)
for {
var out Event
if err := dec.Decode(&out); err != nil {
break
}
fmt.Printf("%s=%d\n", out.Service, out.Status)
}
}
Output: billing=200 billing=200 billing=500
func NewStreamEncoder ¶
func NewStreamEncoder(w io.Writer, mode Mode) *StreamEncoder
NewStreamEncoder returns a stream encoder backed by w. Dense mode activates the full balanced codec set (OptBalanced); Fast mode emits raw tagged bytes (OptSpeed). For finer per-stream tuning, build the StreamEncoder via NewStreamEncoderWith.
func NewStreamEncoderWith ¶
func NewStreamEncoderWith(w io.Writer, opts Options) *StreamEncoder
NewStreamEncoderWith returns a stream encoder with the given Options bit-mask. The intern table (when OptDense is set) survives across Encode calls so back-references span the whole stream.
func (*StreamEncoder) Close ¶
func (s *StreamEncoder) Close() error
Close flushes pending data and releases the scratch buffer to the pool. The underlying writer is not closed. Idempotent: subsequent calls are a safe no-op.
func (*StreamEncoder) Encode ¶
func (s *StreamEncoder) Encode(v any) error
Encode writes v as the next value in the stream. The encoder flushes internally when its buffer crosses 16 KiB; call Flush to push earlier.
func (*StreamEncoder) Flush ¶
func (s *StreamEncoder) Flush() error
Flush writes any buffered bytes to the underlying writer.
type Unmarshaler ¶
Unmarshaler is implemented by types that know how to deserialize themselves from a QDF wire-format slice. Implementations should consume exactly one value from src and return the number of bytes consumed.
Source Files
¶
- columnar.go
- decoder.go
- encoder.go
- errors.go
- floats_default.go
- maps_fast.go
- maps_fast_generated.go
- marshaler.go
- qdf.go
- qdf_direct.go
- qdf_generic.go
- qpack.go
- qpack_alp.go
- qpack_bitpack_fast.go
- qpack_delta.go
- qpack_dict.go
- qpack_for.go
- qpack_gorilla.go
- qpack_pfor.go
- qpack_rle.go
- qpack_simd_stub.go
- race_norace.go
- reflect_encode.go
- slices_fast.go
- state.go
- stream.go
- wire.go
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
bufpool
Package bufpool implements size-classed, sharded byte-slice pools.
|
Package bufpool implements size-classed, sharded byte-slice pools. |
|
endian
Package endian exposes a build-time constant for native byte order.
|
Package endian exposes a build-time constant for native byte order. |
|
intern
Package intern provides a small fixed-size string cache used by the decoder to deduplicate repeated map keys.
|
Package intern provides a small fixed-size string cache used by the decoder to deduplicate repeated map keys. |
|
internarena
Package internarena is a bytes-only bump-pointer allocator for the Dense-mode intern table.
|
Package internarena is a bytes-only bump-pointer allocator for the Dense-mode intern table. |
|
mapsgen
command
internal/mapsgen — code generator for typed map encode/decode fast paths.
|
internal/mapsgen — code generator for typed map encode/decode fast paths. |
|
rans
Package rans is a static order-0 byte range Asymmetric Numeral System coder (the canonical rans_byte: 32-bit state, byte renormalization, a 12-bit normalized frequency table).
|
Package rans is a static order-0 byte range Asymmetric Numeral System coder (the canonical rans_byte: 32-bit state, byte renormalization, a 12-bit normalized frequency table). |
|
reflectutil
Package reflectutil holds the reflect-based helpers the qdf codec uses to allocate slices and maps without going through reflect.Value.
|
Package reflectutil holds the reflect-based helpers the qdf codec uses to allocate slices and maps without going through reflect.Value. |
|
unsafestr
Package unsafestr provides zero-copy conversions between string and []byte.
|
Package unsafestr provides zero-copy conversions between string and []byte. |