Artifact 854bf3308de9950ef49709c72ab861dd2fb1873bf5959a00d38106a8b7239ce6:
- File
internal/delta/delta.go
— part of check-in
[8b16c91f5c]
at
2026-07-28 04:03:58
on branch trunk
— perf: stop clone re-seeking blob.uuid per F-card and over-allocating delta output (#187) (#212)
Cloning the Fossil SCM corpus (67.6k blobs) took 293-302 s at 1251-1287 MB against canonical fossil's 20.8 s at 205 MB. A CPU and allocation profile of one clone found the cost was not delta expansion or hash verification.
27.2% of all CPU was a single un-memoized statement. content.AvailabilityCache memoizes the delta-chain walk but not the "SELECT rid FROM blob WHERE uuid=?" that starts it, so manifest.missingCheckinRefs re-seeked the blob.uuid index once per F-card of every check-in -- the same file hash resolved again for every revision that did not change it. That index is far larger than SQLite's page cache, so each seek was three page reads: pread syscalls were 34.7% of total samples. The walk it guards was under 1% of its own cost.
Separately, materializing 8.27 GB of content allocated 360 GB. delta.ApplyInto starts at a 64 KiB cap deliberately -- the target length is a wire claim and a few bytes can declare 4 GiB -- and then climbs to the real size through append's ~1.25x large-slice growth, ~3.4x the target in discarded intermediates, leaving slack that content.Cache.clip copied away again (203.9 GB and 59.8 GB respectively).
Three changes:
- Memoize uuid -> rid in AvailabilityCache. Hits only. Within one crosslink run a hit cannot go stale, because a sweep and a cascade only ever add blob rows; a miss can, because ridOrPhantom and blob.StorePhantom create rows mid-run, and remembering an absence would leave an artifact deferred after the content it waits on arrived. The scope that makes this sound is now stated on the type, and TestAvailabilityCacheMustNotSpanAPurge pins its one boundary: shun.Purge deletes blob rows, so a cache must not span one.
- Size delta.ApplyInto's output exactly. scanOutputSize walks the command stream first without allocating, checking every copy against bytes the source holds and every insert against bytes the delta carries, so the size it returns is a measurement rather than the header's claim. Any stream it cannot walk to a clean terminator falls back to the existing growth path, so malformed and hostile deltas allocate and fail exactly as before. The buffer now arrives tightly sized, which also makes clip's copy a no-op.
- Cut crosslinkCacheBytes from 256 to 128 MiB. Each byte of that budget costs roughly five bytes of peak RSS -- GOGC doubles it into the heap goal, and the scavenger is paced off GC cycles, of which a faster sweep runs fewer. At 256 MiB the first two changes gave 205.7 s but 1473 MB, faster at the cost of memory. The measured curve (32/64/128/256 MiB: 369.6 s/318 MB, 292.3 s/447 MB, 249.4 s/812 MB, 205.7 s/1473 MB) puts 128 MiB where both axes improve.
Result on the same corpus: 245.4 s at 780 MB, from 293-302 s at 1251-1287 MB -- 17% less wall time and 39% less peak RSS, with instructions retired down from 3.37T to 2.48T. The gap to canonical narrows from 14.1x/6.3x to 11.8x/3.8x.
Derived tables are unchanged against an unmodified build cloning the same server: blob 67530, event 27099 (identical per type), mlink 104919, plink 23741, tagxref 51766, leaf 1426, filename 1496. The corpus parity gate passes.
What remains is structural rather than a missing optimization: a clone expands every blob twice, once in the receive-time phantom-fill cascade and once in the final whole-repository sweep, where canonical crosslinks each artifact from content it already holds in hand at receive time. Cache.Expand splits 45.4 s / 35.3 s across those two passes. Collapsing them is a change to the receive path and belongs in its own issue. (user: [email protected], size: 21735) [annotate] [blame] [check-ins using]
package delta import ( "errors" "fmt" "math" ) var ( ErrInvalidDelta = errors.New("delta: invalid format") ErrChecksum = errors.New("delta: checksum mismatch") ) // maxDeltaTargetSize bounds the target length declared in a delta header. // It is far larger than any real Fossil artifact — its purpose is to // reject an obviously-hostile or malformed claim (wire data, never // trusted) before it reaches an allocation or a size column, not to // constrain legitimate content. const maxDeltaTargetSize = 1 << 32 // 4 GiB var digits = [128]int{ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, 36, -1, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, -1, -1, -1, 63, -1, } type reader struct { data []byte pos int } func (r *reader) getInt() (uint64, error) { if r == nil { panic("delta.getInt: receiver must not be nil") } var v uint64 started := false for r.pos < len(r.data) { c := r.data[r.pos] if c >= 128 || digits[c] < 0 { break } d := uint64(digits[c]) // Reject rather than wrap: v*64+d overflowing uint64 would wrap // silently, and a wrapped value can land anywhere — including // exactly math.MaxUint64, which casts to int64(-1), the phantom // sentinel used throughout blob.size. Wire data gets no benefit // of the doubt here. if v > (math.MaxUint64-d)/64 { return 0, fmt.Errorf("%w: integer overflow at pos %d", ErrInvalidDelta, r.pos) } v = v*64 + d r.pos++ started = true } if !started { return 0, fmt.Errorf("%w: expected integer at pos %d", ErrInvalidDelta, r.pos) } return v, nil } func (r *reader) getChar() (byte, error) { if r == nil { panic("delta.getChar: receiver must not be nil") } if r.pos >= len(r.data) { return 0, fmt.Errorf("%w: unexpected end at pos %d", ErrInvalidDelta, r.pos) } c := r.data[r.pos] r.pos++ return c, nil } // parseTargetLen reads the "<len>\n" header that begins every delta, // without touching anything past it. Apply continues on into the command // stream from the returned reader position; OutputSize stops here. func parseTargetLen(r *reader) (uint64, error) { targetLen, err := r.getInt() if err != nil { return 0, err } term, err := r.getChar() if err != nil { return 0, err } if term != '\n' { return 0, fmt.Errorf("%w: expected newline after target length", ErrInvalidDelta) } if targetLen > maxDeltaTargetSize { return 0, fmt.Errorf("%w: target length %d exceeds maximum %d", ErrInvalidDelta, targetLen, uint64(maxDeltaTargetSize)) } return targetLen, nil } // OutputSize returns the reconstructed size of a delta's target, read from // the delta header alone. It does not require or touch the source content, // so a delta's expanded size is known even when its base is not yet // available — mirrors Fossil's delta_output_size (src/delta.c). Returns an // error (never panics) on malformed or empty input: deltaBytes is wire // data, not a programmer-controlled argument, so a malformed value is an // input-validation failure, not an assertion failure. func OutputSize(deltaBytes []byte) (uint64, error) { if len(deltaBytes) == 0 { return 0, fmt.Errorf("%w: empty delta", ErrInvalidDelta) } return parseTargetLen(&reader{data: deltaBytes}) } // Apply reconstructs target data from source and delta, allocating a fresh // output buffer. It is ApplyInto with no buffer to reuse. func Apply(source, delta []byte) (result []byte, err error) { return ApplyInto(nil, source, delta) } // ApplyInto reconstructs target data from source and delta into dst, reusing // dst's capacity when it is large enough. dst must be empty (len(dst) == 0); // pass dst[:0] to reuse a buffer from a previous call. A nil dst is the // fresh-allocation path and behaves exactly like Apply. // // Reuse is what makes a delta-chain replay allocate a constant number of // output buffers instead of one per link: the caller ping-pongs two buffers, // each link's target reusing the buffer that held the target two links back. // The reused capacity is proven prior work, not a wire claim, so it is a safe // starting size — unlike targetLen, which is a header value ApplyInto never // trusts to allocate from (see the deltaInitialCap comment below). // // The returned slice may share dst's backing array; the caller must not hold // a reference to dst afterwards expecting it to be independent. // // Variable naming follows fossil/src/delta.c for cross-reference: // cnt = count, offset = source offset, cmd = command byte func ApplyInto(dst, source, delta []byte) (result []byte, err error) { if source == nil { panic("delta.ApplyInto: source must not be nil") } if len(dst) != 0 { panic("delta.ApplyInto: dst must be empty (pass dst[:0])") } defer func() { if err == nil && result == nil { panic("delta.ApplyInto: postcondition violated: result is nil with no error") } }() if len(delta) == 0 { return nil, fmt.Errorf("%w: empty delta", ErrInvalidDelta) } r := &reader{data: delta} targetLen, err := parseTargetLen(r) if err != nil { return nil, err } // The output buffer is never sized from targetLen alone: targetLen is a // claim read from the delta's own header, and maxDeltaTargetSize // (which parseTargetLen already bounds it to) exists to reject // obviously-hostile claims, not to size a safe allocation from — a // handful of wire bytes can declare a target near that bound. Once a // delta can be stored unverified (see blob.StoreDeltaRaw), a peer // plants one such delta and every later Expand of that row repeats // the attempt: a durable, remotely-planted allocation, not a one-off // parse spike. // // There are two ways to keep an allocation tied to work rather than to // the claim, and this takes the better one when it can. scanOutputSize // walks the whole command stream first, checking every command against // bytes that exist, so the size it returns is a measurement -- one exact // allocation, no growth, no slack. Only a stream it cannot walk falls // back to starting small and growing, which tracks work performed too but // pays several times the target in discarded intermediates to do it. const deltaInitialCap = 64 * 1024 // 64 KiB; upgraded once real work is proven (see growOutputCap) var output []byte capped := false switch proven, exact := scanOutputSize(r.data[r.pos:], len(source), targetLen); { case exact: // cap(dst) > 0 also screens out a nil dst, which dst[:0] would carry // through to a nil result for an empty target. if cap(dst) > 0 && cap(dst) >= proven { output = dst[:0] } else { output = make([]byte, 0, proven) } case cap(dst) == 0: initialCap := targetLen if initialCap > deltaInitialCap { initialCap = deltaInitialCap capped = true } output = make([]byte, 0, initialCap) default: // A reused buffer's capacity is proven prior work: reconstructing an // adjacent revision produced it, so it is a legitimate starting size // even above deltaInitialCap. Only when this target outgrows the // inherited buffer does the one-time upgrade fire, and it still sizes // off len(delta) rather than the targetLen header (see growOutputCap). output = dst[:0] capped = uint64(cap(output)) < targetLen } // grownPastInitialCap upgrades output's capacity exactly once, the // first time a command actually executes. Go's growth factor for // large slices is ~1.25x, not 2x, so climbing from a 64 KiB cap to a // multi-megabyte target purely via append's own doubling lands near // 5x the final size in cumulative allocation, not ~1x. A single // upgrade sized off len(delta) — bytes the peer actually had to send, // not the unverified targetLen header — restores near-exact sizing // for honest deltas without reopening the tiny-payload-huge-header // attack: a hostile delta with no real commands never reaches this // upgrade at all (see TestApply_TinyPayloadHugeHeaderBoundsAllocation). grownPastInitialCap := false for r.pos < len(r.data) { cnt, err := r.getInt() if err != nil { return nil, err } cmd, err := r.getChar() if err != nil { return nil, err } switch cmd { case '@': // Fossil delta format: count@offset, (first int = byte count, second = source offset) offset, err := r.getInt() if err != nil { return nil, err } term, err := r.getChar() if err != nil { return nil, err } if term != ',' { return nil, fmt.Errorf("%w: expected comma in copy command", ErrInvalidDelta) } // Bounds check before casting to int if offset > uint64(len(source)) || cnt > uint64(len(source)) { return nil, fmt.Errorf("%w: copy offset/count overflow", ErrInvalidDelta) } if int(offset+cnt) > len(source) { return nil, fmt.Errorf("%w: copy exceeds source bounds (offset=%d, cnt=%d, srclen=%d)", ErrInvalidDelta, offset, cnt, len(source)) } if uint64(len(output))+cnt > targetLen { return nil, fmt.Errorf("%w: output size %d exceeds declared target size %d during copy command", ErrInvalidDelta, uint64(len(output))+cnt, targetLen) } output = append(output, source[offset:offset+cnt]...) if capped && !grownPastInitialCap { output = growOutputCap(output, targetLen, len(delta)) grownPastInitialCap = true } case ':': // Bounds check before casting to int if cnt > uint64(len(r.data)) { return nil, fmt.Errorf("%w: insert count overflow", ErrInvalidDelta) } if r.pos+int(cnt) > len(r.data) { return nil, fmt.Errorf("%w: insert exceeds delta bounds", ErrInvalidDelta) } if uint64(len(output))+cnt > targetLen { return nil, fmt.Errorf("%w: output size %d exceeds declared target size %d during insert command", ErrInvalidDelta, uint64(len(output))+cnt, targetLen) } output = append(output, r.data[r.pos:r.pos+int(cnt)]...) r.pos += int(cnt) if capped && !grownPastInitialCap { output = growOutputCap(output, targetLen, len(delta)) grownPastInitialCap = true } case ';': if uint64(len(output)) != targetLen { return nil, fmt.Errorf("%w: output size %d != target size %d", ErrInvalidDelta, len(output), targetLen) } if cnt != uint64(Checksum(output)) { return nil, fmt.Errorf("%w: expected %d, got %d", ErrChecksum, Checksum(output), cnt) } return output, nil default: return nil, fmt.Errorf("%w: unknown command '%c' at pos %d", ErrInvalidDelta, cmd, r.pos-1) } } return nil, fmt.Errorf("%w: missing terminator", ErrInvalidDelta) } // scanOutputSize walks a delta's command stream and returns the exact number // of bytes it emits, without allocating and without producing any output. // cmds is the stream that follows the header; srcLen and targetLen are the // same bounds the replay loop enforces. // // It exists to close the gap between "how much output will this delta // produce" and "how much may we safely allocate before we know". The header's // targetLen answers the first question but cannot be trusted for the second: // a few wire bytes can declare a 4 GiB target (see the deltaInitialCap comment // in ApplyInto). This walk answers both. Every copy command is checked against // bytes the source actually holds and every insert against bytes the delta // actually carries, so a stream that reaches the terminator with the declared // total has proven, byte for byte, that a buffer of that size is warranted -- // a hostile delta with no real commands never gets past the first command and // is refused an allocation exactly as before. // // The payoff is not the single allocation but the absence of the growth path. // Reconstructing a multi-megabyte target through append's ~1.25x growth from a // 64 KiB start allocates several times the target's size in discarded // intermediates and leaves the survivor with slack capacity that // content.Cache then copies away; on the Fossil SCM repository that was // 200 GiB of delta output allocated to materialize 8 GiB of content. Sized // exactly, the buffer is allocated once and arrives already clipped. // // It reports false -- not an error -- for any stream it cannot walk to a // clean terminator. Diagnosing malformed input is the replay loop's job and it // is about to run anyway; duplicating the error text here would be two places // to keep in step. A false verdict simply falls back to the conservative // growth path, so a malformed delta allocates and fails exactly as it did // before this scan existed. func scanOutputSize(cmds []byte, srcLen int, targetLen uint64) (int, bool) { // maxDeltaTargetSize is 4 GiB, which does not fit an int on a 32-bit // platform -- and js/wasm is one. Declining here leaves such a delta to // the growth path, which fails on it exactly as it always has, rather // than wrapping a size negative. if targetLen > uint64(math.MaxInt) { return 0, false } r := &reader{data: cmds} var total uint64 for r.pos < len(r.data) { cnt, err := r.getInt() if err != nil { return 0, false } cmd, err := r.getChar() if err != nil { return 0, false } switch cmd { case '@': offset, err := r.getInt() if err != nil { return 0, false } term, err := r.getChar() if err != nil || term != ',' { return 0, false } if offset > uint64(srcLen) || cnt > uint64(srcLen) || offset+cnt > uint64(srcLen) { return 0, false } case ':': if cnt > uint64(len(r.data)) || r.pos+int(cnt) > len(r.data) { return 0, false } r.pos += int(cnt) case ';': // The terminator's count is the checksum, not a length; the // replay loop verifies it against the bytes it produced. if total != targetLen { return 0, false } return int(total), true default: return 0, false } if total+cnt > targetLen { return 0, false } total += cnt } return 0, false // no terminator } // deltaGrowthFactor bounds the one-time capacity upgrade growOutputCap // performs against len(delta): the multiple is generous enough to reach // most legitimate targets in a single reallocation, while still scaling // with bytes the peer actually transmitted rather than with an unverified // header claim. const deltaGrowthFactor = 64 // growOutputCap upgrades output's capacity once a command has proven the // delta contains real work, from the conservative 64 KiB initial cap to // min(targetLen, deltaGrowthFactor*len(delta)). targetLen is already // bounded by maxDeltaTargetSize and, from this point in Apply, output can // never be allowed past it either (see the per-command bound checks // above) — so this upgrade can only reduce the number of later appends, // never let output outgrow what was already going to be permitted. func growOutputCap(output []byte, targetLen uint64, deltaLen int) []byte { next := uint64(deltaLen) * deltaGrowthFactor if next > targetLen { next = targetLen } if next <= uint64(cap(output)) { return output } grown := make([]byte, len(output), next) copy(grown, output) return grown } // Checksum computes Fossil's delta checksum, matching delta.c's checksum(). // Sum of 4-byte big-endian words, with trailing bytes in big-endian position. func Checksum(data []byte) uint32 { if data == nil { panic("delta.Checksum: data must not be nil") } var sum uint32 i := 0 n := len(data) for n >= 4 { sum += uint32(data[i])<<24 | uint32(data[i+1])<<16 | uint32(data[i+2])<<8 | uint32(data[i+3]) i += 4 n -= 4 } switch n { case 3: sum += uint32(data[i+2]) << 8 fallthrough case 2: sum += uint32(data[i+1]) << 16 fallthrough case 1: sum += uint32(data[i]) << 24 } return sum } const zDigitsEnc = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~" // nHash is the width of the window the encoder hashes to find landmarks, // matching NHASH in fossil/src/delta.c. The hash table is keyed on windows // of this size and a copy command is only worth emitting for a match at // least this long. const nHash = 16 type hashEntry struct { offset int next int } // Create generates a delta that transforms source into target. // Variable naming follows fossil/src/delta.c for cross-reference: // nHash = NHASH, ei = entry index, ml = match length, tPos = target position, sOff = source offset func Create(source, target []byte) (result []byte) { if source == nil { panic("delta.Create: source must not be nil") } if target == nil { panic("delta.Create: target must not be nil") } defer func() { if len(result) == 0 { panic("delta.Create: postcondition violated: result is empty") } }() if len(target) == 0 { var buf []byte buf = appendInt(buf, 0) buf = append(buf, '\n') buf = appendInt(buf, uint64(Checksum(target))) buf = append(buf, ';') return buf } if len(source) < nHash { return createInsertAll(target) } heads, entries, mask := buildHashTable(source) return emitMatches(source, target, heads, entries, mask) } func buildHashTable(source []byte) (heads []int, entries []hashEntry, mask int) { tableSize := len(source) / nHash if tableSize < 64 { tableSize = 64 } for tableSize&(tableSize-1) != 0 { tableSize &= tableSize - 1 } tableSize <<= 1 mask = tableSize - 1 heads = make([]int, tableSize) entries = make([]hashEntry, 0, len(source)/nHash) for i := 0; i+nHash <= len(source); i += nHash { h := rollingHash(source[i : i+nHash]) idx := int(h) & mask entries = append(entries, hashEntry{offset: i, next: heads[idx] - 1}) heads[idx] = len(entries) } return heads, entries, mask } // maxChainCandidates bounds how many hash-bucket collision-chain entries the // encoder inspects for a single target position. rollingHash is a weak // multiply-accumulate over an nHash-byte window, so low-entropy input — // repeated tokens, runs of whitespace, boilerplate — funnels an enormous // number of source windows into one bucket, and walking every one of them at // every target position makes Create quadratic in target length. Fossil's // delta_create (src/delta.c) bounds the identical loop with `int limit = 250`, // deliberately settling for a possibly-shorter match rather than letting a // pathological chain dominate; match that limit so the trade behaves the same. const maxChainCandidates = 250 // bestMatch returns the longest run of source matching target[tPos:], along // with that run's offset in source, searching the collision chain for the // hash of target[tPos:tPos+nHash]. Callers must ensure that window is in // bounds. bestLen is 0 when no candidate matches at least nHash bytes. // // examined reports how many chain entries were inspected, never more than // maxChainCandidates. emitMatches has no use for it; it is what lets the // regression test assert the walk stays bounded without timing anything. func bestMatch(source, target []byte, tPos int, heads []int, entries []hashEntry, mask int) (bestOff, bestLen, examined int) { idx := int(rollingHash(target[tPos:tPos+nHash])) & mask for ei := heads[idx]; ei > 0 && examined < maxChainCandidates; ei = entries[ei-1].next + 1 { examined++ sOff := entries[ei-1].offset if sOff+nHash > len(source) { continue } if ml := matchLen(source[sOff:], target[tPos:]); ml >= nHash && ml > bestLen { bestLen = ml bestOff = sOff } } return bestOff, bestLen, examined } func emitMatches(source, target []byte, heads []int, entries []hashEntry, mask int) []byte { var buf []byte buf = appendInt(buf, uint64(len(target))) buf = append(buf, '\n') var pendingInsert []byte tPos := 0 flushInsert := func() { if len(pendingInsert) > 0 { buf = appendInt(buf, uint64(len(pendingInsert))) buf = append(buf, ':') buf = append(buf, pendingInsert...) pendingInsert = pendingInsert[:0] } } for tPos < len(target) { var bestLen, bestOff int if tPos+nHash <= len(target) { bestOff, bestLen, _ = bestMatch(source, target, tPos, heads, entries, mask) } if bestLen >= nHash { flushInsert() // Fossil delta format: count@offset, buf = appendInt(buf, uint64(bestLen)) buf = append(buf, '@') buf = appendInt(buf, uint64(bestOff)) buf = append(buf, ',') tPos += bestLen } else { pendingInsert = append(pendingInsert, target[tPos]) tPos++ } } flushInsert() buf = appendInt(buf, uint64(Checksum(target))) buf = append(buf, ';') return buf } func createInsertAll(target []byte) []byte { if len(target) == 0 { panic("delta.createInsertAll: target length must be > 0") } var buf []byte buf = appendInt(buf, uint64(len(target))) buf = append(buf, '\n') buf = appendInt(buf, uint64(len(target))) buf = append(buf, ':') buf = append(buf, target...) buf = appendInt(buf, uint64(Checksum(target))) buf = append(buf, ';') return buf } func appendInt(buf []byte, v uint64) []byte { if v == 0 { return append(buf, '0') } var tmp [13]byte i := len(tmp) for v > 0 { i-- tmp[i] = zDigitsEnc[v&0x3f] v >>= 6 } return append(buf, tmp[i:]...) } func rollingHash(data []byte) uint32 { if len(data) == 0 { panic("delta.rollingHash: data length must be > 0") } var h uint32 for _, b := range data { h = h*37 + uint32(b) } return h } func matchLen(a, b []byte) int { if a == nil { panic("delta.matchLen: a must not be nil") } if b == nil { panic("delta.matchLen: b must not be nil") } n := len(a) if len(b) < n { n = len(b) } for i := 0; i < n; i++ { if a[i] != b[i] { return i } } return n }