Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

277 lines
7.6 KiB
Go

// Copyright 2021 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This file incorporates work covered by the following copyright and
// permission notice:
//
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package tree
import (
"crypto/sha512"
"encoding/binary"
"math"
"math/bits"
"github.com/kch42/buzhash"
"github.com/zeebo/xxh3"
)
const (
minChunkSize = 1 << 9
maxChunkSize = 1 << 14
)
var levelSalt = [...]uint64{
saltFromLevel(1),
saltFromLevel(2),
saltFromLevel(3),
saltFromLevel(4),
saltFromLevel(5),
saltFromLevel(6),
saltFromLevel(7),
saltFromLevel(8),
saltFromLevel(9),
saltFromLevel(10),
saltFromLevel(11),
saltFromLevel(12),
saltFromLevel(13),
saltFromLevel(14),
saltFromLevel(15),
}
// splitterFactory makes a nodeSplitter.
type splitterFactory func(level uint8) nodeSplitter
var defaultSplitterFactory splitterFactory = newKeySplitter
// nodeSplitter decides where Item streams should be split into chunks.
type nodeSplitter interface {
// Append provides more nodeItems to the splitter. Splitter's make chunk
// boundary decisions based on the Item contents. Upon return, callers
// can use CrossedBoundary() to see if a chunk boundary has crossed.
Append(key, values Item) error
// CrossedBoundary returns true if the provided nodeItems have caused a chunk
// boundary to be crossed.
CrossedBoundary() bool
// Reset resets the state of the splitter.
Reset()
}
// rollingHashSplitter is a nodeSplitter that makes chunk boundary decisions using
// a rolling value hasher that processes Item pairs in a byte-wise fashion.
//
// rollingHashSplitter uses a dynamic hash pattern designed to constrain the chunk
// Size distribution by reducing the likelihood of forming very large or very small
// chunks. As the Size of the current chunk grows, rollingHashSplitter changes the
// target pattern to make it easier to match. The result is a chunk Size distribution
// that is closer to a binomial distribution, rather than geometric.
type rollingHashSplitter struct {
bz *buzhash.BuzHash
offset uint32
window uint32
salt byte
crossedBoundary bool
}
const (
// The window Size to use for computing the rolling hash. This is way more than necessary assuming random data
// (two bytes would be sufficient with a target chunk Size of 4k). The benefit of a larger window is it allows
// for better distribution on input with lower entropy. At a target chunk Size of 4k, any given byte changing
// has roughly a 1.5% chance of affecting an existing boundary, which seems like an acceptable trade-off. The
// choice of a prime number provides better distribution for repeating input.
rollingHashWindow = uint32(67)
)
var _ nodeSplitter = &rollingHashSplitter{}
func newRollingHashSplitter(salt uint8) nodeSplitter {
return &rollingHashSplitter{
bz: buzhash.NewBuzHash(rollingHashWindow),
window: rollingHashWindow,
salt: byte(salt),
}
}
var _ splitterFactory = newRollingHashSplitter
// Append implements NodeSplitter
func (sns *rollingHashSplitter) Append(key, value Item) (err error) {
for _, byt := range key {
_ = sns.hashByte(byt)
}
for _, byt := range value {
_ = sns.hashByte(byt)
}
return nil
}
func (sns *rollingHashSplitter) hashByte(b byte) bool {
sns.offset++
if sns.crossedBoundary {
return true
}
sns.bz.HashByte(b ^ sns.salt)
if sns.offset < minChunkSize {
return true
}
if sns.offset > maxChunkSize {
sns.crossedBoundary = true
return true
}
hash := sns.bz.Sum32()
patt := rollingHashPattern(sns.offset)
sns.crossedBoundary = hash&patt == patt
return sns.crossedBoundary
}
// CrossedBoundary implements NodeSplitter
func (sns *rollingHashSplitter) CrossedBoundary() bool {
return sns.crossedBoundary
}
// Reset implements NodeSplitter
func (sns *rollingHashSplitter) Reset() {
sns.crossedBoundary = false
sns.offset = 0
sns.bz = buzhash.NewBuzHash(sns.window)
}
func rollingHashPattern(offset uint32) uint32 {
shift := 15 - (offset >> 10)
return 1<<shift - 1
}
// keySplitter is a nodeSplitter that makes chunk boundary decisions on the hash of
// the key of an Item pair.
//
// keySplitter uses a dynamic threshold modeled on a weibull distribution
// (https://en.wikipedia.org/wiki/Weibull_distribution). As the size of the current
// trunk increases, it becomes easier to pass the threshold, reducing the likelihood
// of forming very large or very small chunks.
type keySplitter struct {
count, size uint32
crossedBoundary bool
salt uint64
}
func newKeySplitter(level uint8) nodeSplitter {
return &keySplitter{
salt: levelSalt[level],
}
}
var _ splitterFactory = newKeySplitter
func (ks *keySplitter) Append(key, value Item) error {
thisSize := uint32(len(key) + len(value))
ks.size += thisSize
if ks.size < minChunkSize {
return nil
}
if ks.size > maxChunkSize {
ks.crossedBoundary = true
return nil
}
// TODO: is there a way to reduce weibullChecks?
h := xxHash32(key, ks.salt)
ks.crossedBoundary = weibullCheck(ks.size, thisSize, h)
return nil
}
func (ks *keySplitter) CrossedBoundary() bool {
return ks.crossedBoundary
}
func (ks *keySplitter) Reset() {
ks.size = 0
ks.crossedBoundary = false
}
const (
targetSize float64 = 4096
maxUint32 float64 = math.MaxUint32
// weibull params
K = 4.
// TODO: seems like this should be targetSize / math.Gamma(1 + 1/K).
L = targetSize
)
// weibullCheck returns true if we should split
// at |hash| for a given record inserted into a
// chunk of size |size|, where the record's size
// is |thisSize|. |size| is the size of the chunk
// after the record is inserted, so includes
// |thisSize| in it.
//
// weibullCheck attempts to form chunks whose
// sizes match the weibull distribution.
//
// The logic is as follows: given that we haven't
// split on any of the records up to |size - thisSize|,
// the probability that we should split on this record
// is (CDF(end) - CDF(start)) / (1 - CDF(start)), or,
// the percentage of the remaining portion of the CDF
// that this record actually covers. We split is |hash|,
// treated as a uniform random number between [0,1),
// is less than this percentage.
func weibullCheck(size, thisSize, hash uint32) bool {
// Instead of using constant K = 4, we just manually multiply to avoid math.Pow call
pow := float64(size-thisSize) / L
start := -math.Expm1(-(pow * pow * pow * pow))
pow = float64(size) / L
end := -math.Expm1(-(pow * pow * pow * pow))
p := float64(hash) / maxUint32
d := 1 - start
if d <= 0 {
return true
}
target := (end - start) / d
return p < target
}
func xxHash32(b []byte, salt uint64) uint32 {
return uint32(xxh3.HashSeed(b, salt))
}
func saltFromLevel(level uint8) (salt uint64) {
full := sha512.Sum512([]byte{level})
return binary.LittleEndian.Uint64(full[:8])
}
// DeterministicHashLevel takes a key and counts the number of leading zeros in the key's hash.
// This is used for computing the level that a key appears in, in a ProximityMap
func DeterministicHashLevel(leadingZerosPerLevel uint8, key Item) uint8 {
h := xxHash32(key, levelSalt[1])
return uint8(bits.LeadingZeros32(h)) / leadingZerosPerLevel
}