Documentation
¶
Overview ¶
Package trace is the binary codec for lap traces: the format described by decision D-1 of docs/SERVER.md, and the reason this module exists.
A 300-sample lap is twelve small integers per sample, about 30 kB as JSON. Stored as
structure of arrays → zig-zag delta → varint → zstd
the same lap is one to two kilobytes, because speed, throttle and track position all move smoothly and a smooth channel deltas down to a handful of bits per sample. The twelve channels are laid down as twelve independent streams rather than interleaved, so each stream contains only values of one magnitude and one shape, which is what makes the deltas small and the entropy coder effective.
Format ¶
A blob is
byte 0 codec version, currently 1 bytes 1…k the point count, as an unsigned varint bytes k… one zstd frame
and the plaintext of that frame is the twelve streams, back to back, in the field order of wire.TracePoint — t, v, thr, brk, g, r, st, p, lg, og, la, lo. Each stream is exactly n values; each value is the difference from its predecessor in the same stream, the first taken against zero, zig-zag mapped to an unsigned integer and written as a varint. Streams carry no length of their own: the count in the header says how many values each holds, which is one number to bounds-check instead of twelve.
The version byte is outside the compressed frame on purpose, so Version can answer without decompressing anything — the server stores it in a neighbouring smallint column and can migrate blobs without reading them. The point count is outside it too, so a blob claiming more than MaxPoints samples is refused before a single byte is decompressed.
Hostile input ¶
Decode is the only part of this module that reads bytes it did not write. Every length and every offset is bounds-checked, the decompressor is capped at MaxPoints worth of plaintext, an over-long input is rejected before zstd is invoked at all, and the decoded trace is put through the same Validate that Encode applies. A truncated, corrupt or hostile blob returns an error. It never panics, and it never returns a trace that would not have been legal to encode.
Allocation ¶
Encode performs one allocation: the returned slice, sized exactly. Decode performs none at all when the caller's destination slice is long enough, because it fills that slice in place and takes its scratch space from a sync.Pool. That is what lets the server's lap-ingest path decode forty laps without touching the heap.
Compatibility ¶
The golden vectors in testdata/golden are the contract, and testdata/README.md says what may and may not change about them.
Index ¶
Constants ¶
const ( MaxOffsetMs = 3_600_000 // t, ms: one hour MaxDistPct = 1_000 // p, ‰ of the lap MaxSpeedKmh = 700 // v, km/h MaxPercent = 100 // thr and brk, % MinGear = -1 // g, reverse MaxGear = 10 // g MaxRPM = 30_000 // r, rpm MaxSteerDeg = 1_000 // st, degrees, symmetric MaxAccelG = 1_000 // lg and og, g × 100, symmetric MaxLat = 9_000_000 // la, degrees × 10⁵, symmetric MaxLon = 18_000_000 )
The bounds of every channel, from the "Trace format" table of API-V1.md. A value outside them is refused by Validate, on the way in and on the way out.
Four of these the specification leaves open, and closing them is this package's decision, recorded here because it is a decision and not a reading:
- t is documented only as "≥ 0". MaxOffsetMs is one hour, far longer than any lap of any circuit, and short enough that the difference between two offsets cannot overflow.
- r is documented only as "≥ 0". MaxRPM is 30000, above any racing engine and any plausible sensor glitch.
- la and lo are given no range at all. They are degrees × 10⁵, so the range is the range of latitude and longitude on Earth.
All four are wide enough that no honest sample reaches them, which is the test a bound like this has to pass.
const CodecVersion = 1
CodecVersion is the version byte at the head of every blob this package writes. It is the number the server keeps in its trace_codec column, and the only value Decode accepts.
Bumping it is the one way the format is allowed to change, and it is a change that must be made deliberately: see testdata/README.md.
const MaxEncodedBytes = 1 << 20
MaxEncodedBytes is the largest blob Decode will look at. A legal trace at MaxPoints cannot exceed roughly half of it even when every channel is incompressible noise, so this only ever rejects something that was never going to decode — and it rejects it before the decompressor is handed anything.
const MaxPoints = 4096
MaxPoints is the hard ceiling on the number of samples in one encoded trace. It is not the same thing as the server's advertised Limits.TracePoints, which is a policy a deployment chooses and which is typically 300: this is the structural bound of the codec itself, the number past which a blob is refused before it is decompressed. It exists so that an attacker cannot hand the server a two-byte header that turns into gigabytes of samples.
Variables ¶
var ErrInvalidTrace = errors.New("trace: invalid trace")
ErrInvalidTrace reports a trace that breaks the rules of the wire format: a channel outside its documented range, or an offset that does not increase. Encode refuses to write one and Decode refuses to return one, so the rule holds in both directions and cannot be smuggled past either side.
var ErrMalformed = errors.New("trace: malformed blob")
ErrMalformed reports a blob that is truncated, corrupt, or carries bytes the format does not account for. It is the answer to every hostile input that is not simply too long.
var ErrTooManyPoints = errors.New("trace: too many points")
ErrTooManyPoints reports a trace, or a blob claiming to hold a trace, longer than MaxPoints.
var ErrVersion = errors.New("trace: unsupported codec version")
ErrVersion reports a blob whose codec version this build does not know. A server holding blobs of an older version keeps the older decoder, or migrates them; it never guesses.
Functions ¶
func Decode ¶
func Decode(b []byte, dst []wire.TracePoint) ([]wire.TracePoint, error)
Decode reads a blob back into dst and returns the result.
dst is reused: when it has room for the trace, Decode fills it in place and allocates nothing at all, which is what the server's ingest path depends on. Pass the slice from the previous call — its length is irrelevant, only its capacity is used — and keep the one Decode returns for the call after that.
Every length and offset in the blob is bounds-checked, the decompressor is capped, and the result is put through Validate before it is returned, so a truncated, corrupt or hostile blob produces an error and never a panic and never a trace that could not have been encoded. On any error dst is returned truncated to zero length rather than dropped, so the caller keeps its buffer.
A trace of no samples comes back as a zero-length slice, which is the caller's own slice emptied and is therefore nil if the caller passed nil. The wire format does not distinguish a nil trace from an empty one and neither does this: compare lengths, not slices.
func Encode ¶
func Encode(pts []wire.TracePoint) ([]byte, error)
Encode writes pts as one self-describing blob.
It validates first: a trace that would not survive the round trip — a channel out of range, an offset that does not increase, more than MaxPoints samples — is refused here rather than stored and discovered later by whoever reads it back. A nil or empty slice is legal and encodes to a header and an empty frame.
Exactly one allocation is performed, the returned slice, and it is sized to fit. The scratch space the twelve streams are built in comes from a pool, so a server encoding lap after lap reuses the same few kilobytes for all of them.
func Validate ¶
func Validate(pts []wire.TracePoint) error
Validate reports whether pts may be encoded, and is also applied to everything Decode produces.
The rules are the ones API-V1.md states: at most MaxPoints samples, every channel inside its documented range, and t strictly increasing. A trace with no GPS fix is valid — la and lo are optional as a pair and both zero means absent — so there is no rule about them beyond their range.
It allocates only when it has something to complain about.
Types ¶
This section is empty.