radix

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 14, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package radix implements an in-memory compressed radix tree (Patricia trie) over full S3 object keys, providing filesystem-style navigation by '/' boundary together with precomputed per-directory aggregates.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSnapshotMagic is returned by Load when the file does not begin with
	// the expected magic identifier.
	ErrSnapshotMagic = errors.New("radix: snapshot magic mismatch")
	// ErrSnapshotVersion is returned by Load when the snapshot's format
	// version is not supported by this build.
	ErrSnapshotVersion = errors.New("radix: snapshot version unsupported")
	// ErrSnapshotCorrupt is returned by Load when a record's ID is out of
	// range or repeats one already seen.
	ErrSnapshotCorrupt = errors.New("radix: snapshot corrupt")
)
View Source
var (
	ErrInvalidPrefix = errors.New("radix: prefix must be empty or end with '/'")
	ErrUnsortedBatch = errors.New("radix: batch objects must be sorted ascending and unique")
	ErrBatchRange    = errors.New("radix: batch contains key <= StartFrom")
)

Functions

This section is empty.

Types

type Aggregate

type Aggregate struct {
	Objects int64
	Bytes   ClassBytes
}

Aggregate is a recursive summary over a subtree of the radix tree.

func (Aggregate) Equal

func (a Aggregate) Equal(b Aggregate) bool

Equal reports whether two aggregates carry the same data. Bytes is treated as an unordered multiset since ClassBytes.Add does not sort.

type Batch

type Batch struct {
	StartFrom string
	Objects   []Object
}

Batch is a contiguous range of S3 objects to ingest.

The caller guarantees that no object exists in the bucket whose key lies in (StartFrom, Objects[last].Key] other than those in Objects. Empty StartFrom means the beginning of the bucket. Objects must be sorted ascending by Key.

type ClassByte

type ClassByte struct {
	Class StorageClass
	Size  int64
}

ClassByte is one (storage class, byte-count) pair. It does double duty: as a single file's metadata (storage class + size in bytes) and as one entry in a sparse aggregate over many files of the same class.

type ClassBytes

type ClassBytes []ClassByte

ClassBytes is a sparse, unsorted accumulator keyed by StorageClass. The number of distinct classes per node is small (S3 has roughly a dozen classes, real buckets typically use 1–3), so linear scans beat ordered lookups while keeping per-node memory minimal.

func (*ClassBytes) Add

func (c *ClassBytes) Add(class StorageClass, n int64)

Add adds n bytes (possibly negative) to the bucket for class. Buckets that reach zero are removed; a new bucket with n == 0 is not appended.

func (*ClassBytes) AddAll

func (c *ClassBytes) AddAll(src ClassBytes)

AddAll merges src into *c.

func (ClassBytes) NonZero

func (c ClassBytes) NonZero() []ClassByte

NonZero returns the populated buckets. The receiver already excludes zero buckets, so the return value is the slice itself.

func (ClassBytes) Total

func (c ClassBytes) Total() int64

Total returns the sum of Size across every bucket. It is the byte counterpart to Aggregate.Objects for callers that have already drilled down to a ClassBytes value.

type Entry

type Entry struct {
	Name      string
	IsDir     bool
	Class     StorageClass
	Size      int64
	Aggregate Aggregate
}

Entry is one element of a ListDirectory result. Either a subdirectory (IsDir true, Aggregate populated) or a direct file (IsDir false, Class+Size set).

type Iterator

type Iterator struct {
	// contains filtered or unexported fields
}

Iterator is a forward, single-pass cursor over the alive objects of a Tree, walked in strict ascending lex order of full keys. Suitable for any "give me keys > X" loop — see sim.Bucket.List for the S3 emulation.

Iterator is read-only: tree mutations while iterating are not supported (same constraint as the existing rangeCursor it wraps).

func (*Iterator) Next

func (it *Iterator) Next() (Object, bool)

Next advances the cursor and returns the next Object plus true. When the tree is exhausted it returns the zero Object plus false; subsequent calls keep returning false.

func (*Iterator) SkipTo

func (it *Iterator) SkipTo(skipKey string)

SkipTo raises the iterator's exclusive lower bound so subsequent Next calls emit only keys strictly greater than skipKey. Cannot retreat — values less than or equal to the current bound are ignored. Useful for the delimiter='/'-style "jump past a CommonPrefix's subtree" pattern, where the caller computes a key that bounds the just-emitted subtree from above and uses SkipTo to advance the cursor past it.

func (*Iterator) Walk

func (it *Iterator) Walk(yield func(key []byte, class StorageClass, size int64) bool)

Walk yields every remaining (key, class, size) triple in ascending lex order, invoking yield exactly once per object. key is a VIEW into the iterator's internal path buffer and is INVALIDATED as soon as yield returns — callers that need to retain it must copy the bytes (e.g. string(key) once, outside the hot loop).

Returning false from yield stops the walk. Walk is the zero-alloc-per- object alternative to Next; it is what sim.Bucket uses to avoid 50 M string allocations during a full bucket sweep.

type NodeKind

type NodeKind uint8

NodeKind discriminates a NodeView between the two arena types.

const (
	NodeInternal NodeKind = iota
	NodeLeaf
)

func (NodeKind) String

func (k NodeKind) String() string

String returns "internal" or "leaf" for human display.

type NodeView

type NodeView struct {
	// CID is the tagged child reference as it appears in a parent's
	// children slice. The root internal carries CID=0 (no tag, no parent).
	CID  uint32
	Kind NodeKind
	Edge string

	// Internal-only fields.
	NumChildren  int
	HasDirMarker bool
	DirMarker    ClassByte
	Aggregate    Aggregate

	// Leaf-only fields.
	Class StorageClass
	Size  int64
}

NodeView is a read-only snapshot of one radix node, suitable for debug or visualisation tools. Both internals and leaves share this type. Fields not applicable to the node's Kind are left at their zero values.

type Object

type Object struct {
	Key   string
	Size  int64
	Class StorageClass
}

Object is a single S3 object record.

type StorageClass

type StorageClass uint8

StorageClass enumerates the S3 storage classes returned by ListObjectsV2. Encoding as a uint8 keeps per-node memory tight; ClassByte.Class is one byte rather than the 16 bytes of a string header.

const (
	ClassUnknown StorageClass = iota
	ClassStandard
	ClassStandardIA
	ClassOneZoneIA
	ClassIntelligentTier
	ClassGlacierIR
	ClassGlacier
	ClassDeepArchive
	ClassReducedRedundancy
	ClassExpressOneZone
	ClassSnow
)

func ParseClass

func ParseClass(s string) StorageClass

ParseClass maps an S3 storage-class string to a StorageClass. Unknown strings (and the empty string) collapse to ClassUnknown.

func (StorageClass) String

func (c StorageClass) String() string

String returns the canonical S3 identifier for c.

type Tree

type Tree struct {
	// contains filtered or unexported fields
}

Tree is an in-memory compressed radix tree over S3 object keys.

Branch nodes (with children and per-subtree aggregates) and terminal leaves (single objects with no children) live in two separate chunked arenas. Each chunk is a fixed-size slab whose backing array never moves, so any pointer obtained from [Tree.atInternal] / [Tree.atLeaf] stays valid for the lifetime of the Tree. Children reference one another by uint32 ID: the high bit (leafTag) selects the arena and the low 31 bits index into it.

The root is an internal at ID 0 and is never released. A freelist per arena reuses slots vacated by deletes.

Concurrency: not safe for concurrent use. Callers must externally serialise AddBatch / ListDirectory calls.

func Load

func Load(r io.Reader) (*Tree, error)

Load reads a snapshot previously produced by Save (or any prior v1 writer) and reconstructs the two-arena in-memory tree.

Each record is classified into the internal or leaf arena as it is read (a record is a leaf iff disk-ID != 0, file != nil, no children, no agg). Children references are stored temporarily as disk IDs and rewritten in a second pass via diskToCID once every record has been placed.

Memory cost during load: arenas + diskToCID (4 B × maxID+1). At 50 M objects diskToCID is ~300 MB, an order of magnitude smaller than the tree itself.

func LoadFile

func LoadFile(path string) (*Tree, error)

LoadFile is a convenience wrapper around Load that opens path, parses the snapshot, and returns the resulting tree. The file is closed before returning regardless of success.

func New

func New() *Tree

New returns an empty Tree with the root internal already allocated.

func (*Tree) AddBatch

func (t *Tree) AddBatch(b Batch) error

AddBatch ingests a contiguous range of objects. See Batch for the range-ownership contract. Behavior:

  • keys in the existing tree in (StartFrom, Objects[last].Key] but not in Objects are deleted.
  • keys present in both with differing size/class are updated.
  • keys only in Objects are inserted.

An empty Objects slice is a no-op (range deletion via an empty batch is not supported yet).

Implementation: a rangeCursor streams existing leaves in (StartFrom, hi] lock-step against the incoming batch via a three-way merge. The merge does not mutate the tree directly; it accumulates conflict-free insert/update/ delete groups. Pure-insert batches (the dominant case during first-scan import) skip allocation entirely by handing the input slice straight to [Tree.insertBatch].

func (*Tree) ChildrenOf

func (t *Tree) ChildrenOf(cid uint32) []NodeView

ChildrenOf returns the children of an internal node addressed by the tagged child ID cid. Returns nil for leaf cids (they have no children). Order matches the in-tree storage: ascending by first byte of child edge.

func (*Tree) Export

func (t *Tree) Export(yield func(Object) bool)

Export walks every alive object (regular leaves plus S3 directory-marker dir-marker files held on internals) in lex order, calling yield with each Object. Returning false from yield stops iteration early.

func (*Tree) ListDirectory

func (t *Tree) ListDirectory(prefix string) ([]Entry, error)

ListDirectory returns the immediate children of the logical directory identified by prefix. The prefix must be either "" (root) or end with '/'. Entries are sorted lexicographically.

func (*Tree) NewIterator

func (t *Tree) NewIterator(startAfter string) *Iterator

NewIterator returns a fresh iterator positioned at the first key strictly greater than startAfter. Pass "" to start from the very beginning.

func (*Tree) RootAggregate

func (t *Tree) RootAggregate() Aggregate

RootAggregate returns the aggregate over the whole tree (the radix root's aggregate). O(1) — the aggregate is maintained incrementally on every insert. Callers that just need the total object count or the per-class bytes should use this instead of walking Export.

func (*Tree) RootView

func (t *Tree) RootView() NodeView

RootView returns the root internal as a NodeView. The root's CID is 0 (it has no parent and is the always-allocated entry point into the tree).

func (*Tree) Save

func (t *Tree) Save(w io.Writer) error

Save writes the tree's binary snapshot to w.

The on-disk format is unchanged from v1: a single contiguous ID space, one record per alive node, internals expressed as nodes-with-children, leaves expressed as nodes-with-file-and-no-children. Internals serialise first (so the root keeps disk ID 0), leaves second. Child references in internal records are rewritten via per-arena disk-ID mappings.

To skip free slots without allocating O(nextX) presence bitmaps, Save makes a sorted copy of each freelist (typically tiny — only grows on prune-heavy deletes) and walks it in lock-step with the slot iteration.

func (*Tree) SaveFile

func (t *Tree) SaveFile(path string) (err error)

SaveFile is a convenience wrapper around Tree.Save that writes to path atomically: the snapshot is first written to path+".tmp" and renamed into place on success, so an interrupted run never leaves a half-written file at the destination. Parent directories are created with mode 0755.

func (*Tree) Stats

func (t *Tree) Stats() TreeStats

Stats returns a TreeStats over every alive arena slot.

type TreeStats

type TreeStats struct {
	AliveNodes        int64
	AliveInternals    int64 // count from the internal arena
	AliveLeaves       int64 // count from the leaf arena
	Leaves            int64 // alias of AliveLeaves (kept for output compatibility)
	Internals         int64 // internals with children, no dir-marker file
	InternalsWithFile int64 // internals carrying an S3 dir-marker file
	Empty             int64 // root or detached internals with no children and no file

	NodesWithAgg     int64
	ClassBytePtrs    int64 // == count of nodes with file != nil (each is one *ClassByte heap alloc)
	ClassBytesLen    int64 // sum of len(agg.Bytes)
	ClassBytesCap    int64 // sum of cap(agg.Bytes)
	EdgeBytes        int64 // sum of len(edge)
	ChildrenSlots    int64 // sum of cap(children)
	MaxEdgeLen       int
	MaxChildrenCount int

	EdgeLenHist  [9]int64 // 0,1,2,3-4,5-8,9-16,17-32,33-64,65+
	ChildrenHist [9]int64 // 0,1,2,3,4,5-7,8-15,16-31,32+

	EstHeapInternals  int64 // AliveInternals * sizeof(internal)
	EstHeapLeaves     int64 // AliveLeaves * sizeof(leaf)
	EstHeapNodes      int64 // EstHeapInternals + EstHeapLeaves
	EstHeapEdges      int64 // sum over edges of sizeClass(len(edge))
	EstHeapChildren   int64 // sum over internals of sizeClass(cap(children)*4)
	EstHeapClassBytes int64 // ClassBytePtrs * sizeClass(sizeof(ClassByte))
	EstHeapAggHeaders int64 // NodesWithAgg * sizeClass(sizeof(Aggregate))
	EstHeapAggBytes   int64 // sum over aggs of sizeClass(cap(Bytes)*sizeof(ClassByte))
	EstHeapTotal      int64
}

TreeStats is a memory-accounting snapshot of the tree's in-arena state. All counters are derived by a single linear walk over the chunked arena; no allocations beyond the freelist copy.

EstHeap* fields are best-effort: edge / children-slice / agg-bytes sizes are rounded up to the nearest Go small-object size class so the totals reflect what the allocator actually charged, not the raw byte count.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL