Background
The compressed-scenario pipeline's CI check verifies a structural invariant on the generated bundle's PCAP (net/*.pcap): packet timestamps must be monotonically non-decreasing. That is, reading records in capture order (their order within the file), no packet's timestamp may be smaller than the previous packet's.
The current PCAP timestamp rewriter (rewrite_timestamps in the pcap module) behaves as follows:
- It walks the file's records in capture order.
- It converts each record's real timestamp to the logical timeline and overwrites only the timestamp bytes in place.
- There is no step that reorders records or enforces monotonicity of the output.
As a result, the monotonicity of the output depends entirely on the assumption that the captured input is already monotonic.
Problem
Captures are produced by a tcpdump sidecar on a bridge interface. Due to bridge/kernel timestamping, two adjacent packets can be recorded with timestamps that regress by a few hundred µs up to a few ms. When such a small regression is present in the input, the rewriter does not correct it, so the output PCAP can become non-monotonic, in which case the compressed-pipeline check fails.
This failure depends on capture timing, so it is flaky. In particular, any change that slightly shifts activity scheduling/execution timing (e.g. a runtime/async-related dependency update) can surface this latent problem and fail CI. This actually happened on a dependency-bump PR (#85), where this check failed (the CI run). The key line from the failing log:
FAIL: packet 578 timestamp 2026-05-03T04:20:52.034496+00:00 regresses after previous 2026-05-03T04:20:52.038528+00:00
In other words, the root cause is not an incompatibility in any particular dependency, but a design gap: the rewriter does not guarantee monotonicity of its output.
Goal
Deterministically guarantee that the rewritten PCAP's packet timestamps are always monotonically non-decreasing, regardless of ordering jitter in the captured input.
Requirements (functional)
- For complete (non-truncated) input, output a valid microsecond-resolution PCAP. (Truncated input follows the existing tolerance; see the truncated-tail preservation item under "Preserve existing behavior".)
- The only things allowed to change are record ordering and each record's 8-byte timestamp field.
incl_len, orig_len, and packet payloads must never be altered.
- All records must be preserved, including non-IPv4 records (ARP, IPv6, LLDP, etc.). No record may be dropped or reinterpreted during reassembly.
- Preserve existing behavior:
- Reject nanosecond-resolution PCAP and pcapng input with distinct errors.
- When the final record is truncated, preserve the truncated tail bytes verbatim.
- Replace the file atomically.
- Continue returning the maximum rewritten logical timestamp (used upstream to aggregate
meta.actual_end).
- Keep the info message for timestamps outside the logical window.
- Behave identically for little-endian and big-endian captures.
- (Intended behavior change) Previously, an identity mapping guaranteed byte-identical output unconditionally; under the new behavior, a non-monotonic input causes records to be reordered, so that unconditional guarantee no longer holds. This is intentional. Byte-identity now holds exactly when the rewritten timestamps are already non-decreasing — which an identity mapping over an already-monotonic capture still satisfies, so that case remains byte-identical (see the corresponding item under "Testing requirements"). The rewriter function's doc comment (which currently states the output is "byte-identical to the input except for the eight rewritten timestamp bytes") must be updated accordingly.
Ordering / determinism constraints
- The output order must be a stable sort by the rewritten logical timestamp.
- Records with the same logical timestamp must keep their original capture order (for determinism and minimal reordering).
- Do not use an unstable sort: it makes no contract to preserve the relative order of equal-key (same logical timestamp) records.
- The sort key must be the microsecond value actually written into the record (
ts_sec * 1_000_000 + ts_usec), not the full-precision logical timestamp. The logical timestamp can carry a sub-microsecond remainder that is truncated on write, so keying on it would let two records the output file shows as simultaneous be ordered by a tail that is not present on disk. Keying on the written value keeps "same sort key" and "same bytes in the file" the same predicate, so the tie-preservation rule above is observable in the output.
Implementation guide (follow this approach)
- Keep the current raw byte walk (do not route through the IPv4 parsing path — this preserves non-IPv4 records).
- For each record, overwrite only the timestamp field, leaving the rest of the record untouched. The rewrite may be done in place in the single file buffer; there is no need to copy each record slice (16-byte record header + payload) into its own buffer.
- Collect, for each rewritten record, its sort key paired with a way to locate its bytes — e.g. a
(timestamp, record_bytes) pair, or a (timestamp, start, end) byte range into the in-place-rewritten buffer. The range form avoids one copy per record and yields the same output.
- Stable-sort this buffer by the sort key defined under "Ordering / determinism constraints".
- Reassemble the output file: original global header + records in sorted order + the preserved truncated tail bytes (if any). Reassembly may be skipped when the collected keys are already non-decreasing, since it would reproduce the buffer byte for byte; this is a pure optimization and must not change the output.
- Compute the existing side outputs (max logical timestamp, out-of-window info message) during the walk, as before. Both are order-independent, so reordering does not affect them.
Do not (anti-patterns)
- Do not work around this by sorting the capture file before rewriting; the sort key must be the post-rewrite logical timestamp.
- Do not avoid it by changing tcpdump options or the capture method; capture jitter cannot be fully eliminated, and the rewriter must own the invariant at a single deterministic point.
- Do not make it pass by relaxing/removing the CI check; the check enforces a legitimate invariant of the artifact.
Testing requirements
- Add a unit test that constructs an input where a later record (in capture order) has a timestamp earlier than the preceding record, and verifies the rewritten result is monotonically non-decreasing.
- Include records with equal logical timestamps to also lock in that original order is preserved on ties (stable sort).
- Note that a three-record tie case cannot distinguish a stable sort from an unstable one:
sort_unstable_by_key falls back to insertion sort — which happens to be stable — for short slices. Locking in stability requires a tie-heavy input long enough to reach the pattern-defeating quicksort path.
- Confirm existing rewriter behavior still holds: byte-identity for an already-monotonic identity input, truncated-tail tolerance, endianness preservation, max-timestamp return, non-IPv4 record passthrough, and the various input-rejection cases.
- Non-IPv4 preservation must be asserted on the raw bytes, since the pcap reader drops non-IPv4 records and therefore cannot observe them.
- Both truncation shapes are worth covering: a partial trailing record header, and a complete 16-byte header whose payload is cut short.
Acceptance criteria
- The compressed-pipeline PCAP monotonicity check passes deterministically, regardless of capture jitter.
- New and existing unit tests pass.
cargo fmt and cargo clippy (no warnings) pass.
Non-goals
- Changing capture (tcpdump) configuration.
- Changing the time-compression (real→logical) mapping formula.
- Changing the semantics of the CI check.
Background
The compressed-scenario pipeline's CI check verifies a structural invariant on the generated bundle's PCAP (
net/*.pcap): packet timestamps must be monotonically non-decreasing. That is, reading records in capture order (their order within the file), no packet's timestamp may be smaller than the previous packet's.The current PCAP timestamp rewriter (
rewrite_timestampsin the pcap module) behaves as follows:As a result, the monotonicity of the output depends entirely on the assumption that the captured input is already monotonic.
Problem
Captures are produced by a tcpdump sidecar on a bridge interface. Due to bridge/kernel timestamping, two adjacent packets can be recorded with timestamps that regress by a few hundred µs up to a few ms. When such a small regression is present in the input, the rewriter does not correct it, so the output PCAP can become non-monotonic, in which case the compressed-pipeline check fails.
This failure depends on capture timing, so it is flaky. In particular, any change that slightly shifts activity scheduling/execution timing (e.g. a runtime/async-related dependency update) can surface this latent problem and fail CI. This actually happened on a dependency-bump PR (#85), where this check failed (the CI run). The key line from the failing log:
In other words, the root cause is not an incompatibility in any particular dependency, but a design gap: the rewriter does not guarantee monotonicity of its output.
Goal
Deterministically guarantee that the rewritten PCAP's packet timestamps are always monotonically non-decreasing, regardless of ordering jitter in the captured input.
Requirements (functional)
incl_len,orig_len, and packet payloads must never be altered.meta.actual_end).Ordering / determinism constraints
ts_sec * 1_000_000 + ts_usec), not the full-precision logical timestamp. The logical timestamp can carry a sub-microsecond remainder that is truncated on write, so keying on it would let two records the output file shows as simultaneous be ordered by a tail that is not present on disk. Keying on the written value keeps "same sort key" and "same bytes in the file" the same predicate, so the tie-preservation rule above is observable in the output.Implementation guide (follow this approach)
(timestamp, record_bytes)pair, or a(timestamp, start, end)byte range into the in-place-rewritten buffer. The range form avoids one copy per record and yields the same output.Do not (anti-patterns)
Testing requirements
sort_unstable_by_keyfalls back to insertion sort — which happens to be stable — for short slices. Locking in stability requires a tie-heavy input long enough to reach the pattern-defeating quicksort path.Acceptance criteria
cargo fmtandcargo clippy(no warnings) pass.Non-goals