Skip to content

feat(internal): make level govern match-finding search depth - #52

Merged
jamesarich merged 1 commit into
mainfrom
feat/level-search-depth
Aug 16, 2026
Merged

jamesarich merged 1 commit into
mainfrom
feat/level-search-depth

Conversation

@jamesarich

@jamesarich jamesarich commented Aug 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

level (1..22, zstd's public range) was a complete no-op: the encoder
always used the same fixed match-finding effort regardless of what was
passed. It now maps to a real search-depth knob, governing only how
many candidate distances the matcher examines per position.

Type of change

  • New feature (non-breaking)

Related issue / discussion

Last PR in the encoder-parity series (#47/#48/#50/#51). Independent of
the others — this is a search-depth change only, unrelated to
dict-entropy reuse.

Affirmations

  • All commits are signed off (DCO — git commit -s).
  • I have read CONTRIBUTING.md.
  • If this changes the public API, I have run ./gradlew apiDump and committed the regenerated api/*.api files. — N/A, level's signature was already public; only its (documented no-op) behavior changes.
  • If this changes codec behavior or the wire frame, I have verified round-trip and libzstd interop (./gradlew jvmTest).
  • I have run ./gradlew build locally and it passes.

How was this verified?

  • level == 19 (Zstd.DEFAULT_LEVEL) maps to exactly the previously
    hardcoded values, so compressing at the default level is
    byte-identical to before this change — ByteIdenticalRegressionTest
    needed zero hex changes, which is the actual proof this held.
    Below 19 it interpolates down to a fast floor at level 1; at/above 19
    it interpolates up to a deliberately capped ceiling at level 22
    (bounding worst-case CPU on a highly repetitive payload at high
    effort, rather than left to keep growing). Out-of-range values are
    clamped rather than rejected, matching real zstd's tolerance.
  • Direct checks of the mapping at the floor/default/ceiling, clamping,
    and monotonicity across the whole 1..22 range.
  • Round-trip checks at several levels (dict and dict-less) confirm
    every level still produces a valid, decodable frame — deeper search
    can only change which match gets chosen, never produce an invalid
    sequence.
  • An adoption guard compresses a purpose-built input (a target run
    sandwiched around 60 pairwise-distinct decoy chunks sharing only its
    4-byte hash prefix, forcing a genuine 61-entry-deep hash-chain walk)
    and confirms level 22 finds the real match level 1's shallower floor
    cannot reach, producing a strictly smaller frame — the actual proof
    level does something, not just that it's plumbed through without
    crashing.
  • ./gradlew jvmTest and ./gradlew build (full 13-target matrix)
    both pass.

Notes for reviewers

MatchIndex.forEachCandidate gained a defaulted maxCandidates
parameter instead of always reading the compile-time constant — the
search depth is a per-call knob, not part of the index structure, so
the same index (built once per dictionary) keeps getting reused across
calls at different levels without rebuilding it.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@jamesarich, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e1a78c8e-f17e-433e-9d39-db6ac42f3d7d

📥 Commits

Reviewing files that changed from the base of the PR and between 0bbd77e and 6ad7381.

📒 Files selected for processing (4)
  • src/commonMain/kotlin/org/meshtastic/kzstd/Zstd.kt
  • src/commonMain/kotlin/org/meshtastic/kzstd/internal/MatchIndex.kt
  • src/commonMain/kotlin/org/meshtastic/kzstd/internal/ZstdEncoder.kt
  • src/commonTest/kotlin/org/meshtastic/kzstd/LevelSearchDepthTest.kt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jamesarich
jamesarich marked this pull request as ready for review August 16, 2026 15:56
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch from a5df1c5 to e13a167 Compare August 16, 2026 15:59
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch from e13a167 to bf40dae Compare August 16, 2026 16:06
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch from bf40dae to 5401ac2 Compare August 16, 2026 16:35
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch from 5401ac2 to 8f4d101 Compare August 16, 2026 16:36
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch 2 times, most recently from 9f92db6 to 24688f7 Compare August 16, 2026 16:55
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch 2 times, most recently from 17521d4 to b46dff5 Compare August 16, 2026 17:20
Base automatically changed from feat/treeless-dict-huffman-literals to main August 16, 2026 17:35
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch from b46dff5 to cb36d6c Compare August 16, 2026 17:35
`level` (1..22, zstd's public range) was a complete no-op: the
encoder always used the same fixed maxChain/maxCandidates regardless
of what was passed. It now maps to a real search-depth knob via a new
searchDepthFor(level), governing ONLY how many candidate distances
the matcher examines per position -- not entropy-table choice
(dict-entropy reuse from the Repeat-mode/Treeless-literals work stays
unconditional whenever the dict's tables are covered, independent of
level) and not zstd's other 21 levels' distinct strategies/parameters
(windowLog, targetLength, etc.) -- this encoder still uses one fixed
greedy/lazy strategy at every level, just searching more or less.

level == 19 (Zstd.DEFAULT_LEVEL) maps to exactly (maxChain=64,
maxCandidates=32) -- today's previously-hardcoded values -- so
compressing at the default level is byte-identical to before this
change; ByteIdenticalRegressionTest needed zero hex changes, which is
the actual proof this held. Below 19 it interpolates down to a fast
floor (8, 4) at level 1; at/above 19 it interpolates up to a capped
ceiling (256, 128) at level 22, deliberately capped rather than left
to keep growing, to bound worst-case CPU on a highly repetitive
payload at high effort. Out-of-range level values are clamped rather
than rejected, matching real zstd's tolerance for requests outside
its advertised range.

MatchIndex.forEachCandidate gained a maxCandidates parameter
(defaulted, so any other caller is unaffected) instead of always
reading the compile-time MAX_CANDIDATES constant -- the search depth
is a per-call knob, not part of the index structure, so the same
index (built once per dictionary) keeps getting reused across calls
at different levels without rebuilding it.

Tests: direct checks of searchDepthFor's mapping at the floor (1),
default (19), and ceiling (22), out-of-range clamping, and
monotonicity across the whole 1..22 range. Round-trip checks at
several levels confirm every level still produces a valid, decodable
frame (deeper search can only change which match gets chosen, never
produce an invalid sequence). An adoption guard compresses a
purpose-built input -- a target run sandwiched around 60 pairwise-
distinct decoy chunks sharing only its 4-byte hash prefix, forcing a
genuine 61-entry-deep hash-chain walk -- and confirms level 22 finds
the real match level 1's shallower floor cannot reach, producing a
strictly smaller frame; this is the actual proof `level` does
something, not just that it's plumbed through without crashing.

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
@jamesarich
jamesarich force-pushed the feat/level-search-depth branch from cb36d6c to 6ad7381 Compare August 16, 2026 17:38
@jamesarich
jamesarich added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit a5a31da Aug 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant