\Drupal\strata\Cas\Chunker FastCdcChunker

Content-defined chunking, off by default and priced in the interface.

FastCDC picks boundaries from a rolling hash of the content, so inserting bytes shifts one chunk instead of every chunk after it. That is the one thing fixed-size framing cannot do, and it is why this exists.

It is 120 to 160 times slower than the fixed splitter and that is not a bug to optimise away. Measured in pure PHP on the reference host: 4.29 MB/s reading byte by byte with ord(), 5.63 MB/s over 64 KiB windows unpacked with unpack, against 683 MB/s for fixed framing plus a BLAKE2b per frame. An 80 GB initial file capture would spend 5.9 hours of pure CPU here. PHP has no primitive for a per-byte rolling hash, and the array form that would vectorise it costs roughly 16x the string in memory, which exhausts a 128 MB limit at 8 MB of input.

So this is reached only from file.shift_detected, only when an administrator has turned it on for a file type, only with a byte budget per cron run, and never for media or the operation stream.

Two normalisations matter and are easy to get wrong. The mask is widened for chunks below the target and narrowed above it, which is what makes the size distribution tight rather than exponential - that is the difference between FastCDC and the older Rabin approach, and it is where most of its speed comes from. And the minimum size is skipped without hashing, because hashing bytes that cannot produce a boundary is pure cost.

Summary

Methods
Properties
Constants
__construct
id
throughput
minimum
maximum
chunk
No public properties found
MEASURED_THROUGHPUT
DEFAULT_TARGET
MIN_FRACTION
MAX_MULTIPLE
No protected methods found
No protected properties found
No protected constants found
split
boundary
gear
gear
target
strictMask
lenientMask
BUFFER

Constant

MEASURED_THROUGHPUT

MEASURED_THROUGHPUT = 4290000

Measured throughput on the reference host, in bytes per second.

The per-byte figure rather than the windowed one, because the windowed variant is what exhausts memory on a large buffer and is therefore not what runs.

DEFAULT_TARGET

DEFAULT_TARGET = 16384

Default target chunk size.

MIN_FRACTION

MIN_FRACTION = 0.25

How small a chunk may be, as a fraction of the target.

Below this the per-chunk overhead - a digest, an index row, a pack entry - starts to cost more than the deduplication saves.

MAX_MULTIPLE

MAX_MULTIPLE = 4

How large a chunk may be, as a multiple of the target.

BUFFER

BUFFER = 262144

How much is read from the stream at a time.

Properties

$gear

$gear : list|null

The gear table the rolling hash indexes.

Derived deterministically rather than shipped as a literal, so two hosts produce identical boundaries for identical content. A random table would make a chunk's identity depend on which machine wrote it, which would defeat deduplication across a restore.

Type

array<int, int>|null —

$target

$target : int

Target chunk size in bytes.

Type

int

$strictMask

$strictMask : int

Mask used below the target, with more bits set so a boundary is harder to hit.

Type

int

$lenientMask

$lenientMask : int

Mask used above the target, with fewer bits set so a boundary is easier to hit.

Type

int

Methods

__construct()

__construct(int  $target = \self::DEFAULT_TARGET) : mixed

Constructs a chunker.

Parameters

int $target

Target chunk size in bytes, between 1 KiB and 4 MiB.

Throws

\InvalidArgumentException

When the target is outside the supported range.

Returns

mixed —

id()

id() : string

A short lowercase identifier.

Returns

string —

For example "fastcdc".

throughput()

throughput() : int

Measured throughput on the reference host, in bytes per second.

Returns

int —

Bytes per second.

minimum()

minimum() : int

The smallest chunk this chunker will emit.

Returns

int —

Bytes.

maximum()

maximum() : int

The largest chunk this chunker will emit.

Returns

int —

Bytes.

chunk()

chunk(mixed  $stream, int  $budget = 0) : \Traversable<int,array{offset: int, bytes: string}>

Splits a stream into chunks.

Parameters

mixed $stream

An open, readable stream positioned where splitting should start.

int $budget

Most bytes to read before stopping, so one cron run cannot be consumed by one file. Zero means no limit and is for a command an operator is watching.

Returns

\Traversable

Each chunk's offset in the stream and its content, in order.

split()

split(resource  $stream, int  $budget) : \Generator<int,array{offset: int, bytes: string}>

Splits a stream, yielding each chunk as it is closed.

Parameters

resource $stream

The stream.

int $budget

Most bytes to read; zero for no limit.

Returns

\Generator

The chunks.

boundary()

boundary(string  $buffer, list  $gear, int  $minimum, int  $maximum, int  $hash) : int|null

Where the next chunk ends.

Parameters

string $buffer

The bytes available.

list $gear

The gear table.

int $minimum

Smallest chunk.

int $maximum

Largest chunk.

int $hash

The rolling hash, carried across calls.

Returns

int|null —

The chunk length, or NULL when the buffer holds no boundary and is not yet at the maximum, so the caller should read more before deciding.

gear()

gear() : list<int>

The gear table, built once per process.

Derived from a fixed seed with a simple xorshift, so every host produces the same table and therefore the same boundaries for the same content.

Returns

list

256 values.