⚡ feat: Add SIMD-accelerated search package - #237
Conversation
Port the simd package from github.com/coregx/coregex (MIT License, preserved in simd/LICENSE and per-file headers) as a new exported subpackage: AVX2 assembly kernels on amd64 processing 32 bytes per iteration, with portable SWAR fallbacks everywhere else. Exported API: Memchr2/Memchr3 (multi-needle scan), MemchrPair (fixed-distance pair scan), MemchrDigit/MemchrDigitAt, MemchrWord/MemchrNotWord, MemchrInTable/MemchrNotInTable, IsASCII/ FirstNonASCII/CountNonASCII, Memmem with rare-byte prefiltering (SelectRareBytes/ByteRank/ByteFrequencies), and Accelerated(). Adaptations from upstream: - Replace legacy-SSE MOVD/MOVQ register moves in the kernels with VEX-encoded VMOVD/VMOVQ: the originals dirty the YMM upper state and trigger AVX-SSE transition stalls on Intel CPUs, measured as a constant ~150ns per call (Memchr2 at 512B: 209ns before, 16ns after). - Drop the single-byte Memchr kernel: bytes.IndexByte is already vector-accelerated by the Go runtime and beat it at every size, so single-byte search delegates to the stdlib instead. - Memmem uses the SIMD path only with AVX2 and 128+ byte haystacks (the measured crossover) and otherwise delegates to bytes.Index, so it is never meaningfully slower than the stdlib. - CPU capability comes from a self-contained CPUID/XGETBV probe. Top-level IsASCII, IndexAny2, and IndexAny3 now dispatch to the AVX2 kernels for inputs of 32+ bytes on amd64 via a zero-copy byteSeq view (unsafeconv.Bytes); benchstat -count=10 against the previous SWAR word loops shows -35% to -88% ns/op at 32-512B on an AVX2 Xeon, with sub-32-byte inputs keeping the existing SWAR paths. Fallback paths are compared against scalar references across all dispatch boundaries by sweep tests and five fuzz targets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Replace the hand-rolled CPUID/XGETBV probe with the official golang.org/x/sys/cpu package, which performs the same CPU and OS YMM-state checks and is maintained by the Go team. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Correctness/robustness: - Memmem: cap failed candidate verifications at 16 and fall back to bytes.Index from the first unchecked position, removing the O(n*m) adversarial worst case (1 MiB of 'z' with a 1000-byte almost-matching needle: 47ms -> 1.7ms, now within 2% of bytes.Index; see Benchmark_Memmem_Adversarial). Docs updated to state the bounded fallback instead of overclaiming. - AVX2 kernels: replace the scalar byte-at-a-time tail loops with one overlapping vector at the buffer end in all seven kernels (safe because already-scanned lanes are known non-matching, the same argument as the SWAR overlapping word at n-8). A 63-byte Memchr2 miss drops from 28.1ns to 7.1ns, matching the 64-byte cost. Scalar loops remain only for sub-MinLen direct calls, which the Go dispatch never makes. - Fix the kernel header comments that misdescribed the ABI0 frame (byte params are packed 1-byte slots at +24/+25/+26, not overlapping 8-byte slots). Single sources of truth: - Export simd.MinLen (32) and use it in every dispatch gate, including the top-level IsASCII/IndexAny2/IndexAny3 prologues, so the kernel crossover cannot drift between packages. - Rebuild all portable fallbacks on the swar package primitives (Broadcast/Load8/ZeroLanes/FirstLane/MatchRangeMask/MatchByteMask) instead of hand-rolled constants and formulas; this also upgrades the digit/word-class fallbacks and FirstNonASCII/CountNonASCII from byte-at-a-time loops to word-wide SWAR scans on non-amd64. - Unexport the byte-frequency table behind ByteRank so callers cannot mutate the ranking Memmem's prefilter depends on. Docs and evidence: - Package doc and README now state the acceleration tier of every function (AVX2+SWAR, SWAR-only, or scalar) instead of a blanket claim. - Benchmark_Memmem gains 96/128/192-byte points bracketing the 128-byte bytes.Index crossover that memmemMinHaystack encodes, and an adversarial benchmark pinning the miss-budget fallback. - The arm64 catalog's Search/ASCII sections now point at the amd64 simd numbers below. Test coverage: - Direct tests and fuzzing for memmemPaired/memmemSingle (previously reachable only with AVX2) including the fallback handover, and direct fuzzing of memchrPairGeneric (previously only reachable below the dispatch length on AVX2 hosts). - Test_Accelerated can be made to fail on runners that should have AVX2 via GOFIBER_SIMD_REQUIRE_AVX2=1, so fallback-only CI runs cannot silently green-light the assembly surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #237 +/- ##
==========================================
+ Coverage 89.38% 91.11% +1.73%
==========================================
Files 17 28 +11
Lines 1432 1756 +324
==========================================
+ Hits 1280 1600 +320
- Misses 132 135 +3
- Partials 20 21 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CI's revive flags unexported functions that differ from an exported name only by capitalization (IsASCII/isASCII, Memchr2/memchr2, ...). Rename the per-arch dispatch hooks with an Impl suffix; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
|
Warning Review limit reached
Next review available in: 30 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThis PR adds portable SWAR and amd64 AVX2 byte-search primitives, integrates accelerated paths into ChangesSIMD search package
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant ExistingHelpers
participant UnsafeBytes
participant SIMD
participant AVX2
participant SWAR
Caller->>ExistingHelpers: call IsASCII or IndexAny2/3
ExistingHelpers->>UnsafeBytes: convert string-like input
ExistingHelpers->>SIMD: dispatch eligible byte input
SIMD->>AVX2: use AVX2 when available and input is at least MinLen
SIMD->>SWAR: use portable fallback otherwise
AVX2-->>ExistingHelpers: return search or ASCII result
SWAR-->>ExistingHelpers: return search or ASCII result
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@simd/ascii_amd64.go`:
- Line 16: Suppress the revive confusing-naming lint for the intentional private
dispatchers: apply the same suppression to isASCII in simd/ascii_amd64.go:16-16,
and to both memchrWord and memchrNotWord in simd/memchr_class_amd64.go:20-33, or
add an equivalent revive exclusion covering these symbols/files. Preserve the
existing names and exported wrapper behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4452bcbf-5e88-40bc-b084-057e310f5f68
⛔ Files ignored due to path filters (2)
go.modis excluded by!**/*.modgo.sumis excluded by!**/*.sum,!**/*.sum
📒 Files selected for processing (30)
README.mdascii.gointernal/unsafeconv/unsafeconv.gosearch.gosearch_test.gosimd/LICENSEsimd/ascii.gosimd/ascii_amd64.gosimd/ascii_amd64.ssimd/ascii_other.gosimd/bench_test.gosimd/byte_frequencies.gosimd/cpu_amd64.gosimd/cpu_other.gosimd/fuzz_test.gosimd/memchr.gosimd/memchr_amd64.gosimd/memchr_amd64.ssimd/memchr_class.gosimd/memchr_class_amd64.gosimd/memchr_class_amd64.ssimd/memchr_class_other.gosimd/memchr_digit.gosimd/memchr_digit_amd64.gosimd/memchr_digit_amd64.ssimd/memchr_digit_other.gosimd/memchr_other.gosimd/memmem.gosimd/simd.gosimd/simd_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
- GitHub Check: benchmark / benchmark
- GitHub Check: Build (1.25.x, windows-latest)
- GitHub Check: Build (1.25.x, macos-latest)
- GitHub Check: Build (1.26.x, windows-latest)
🧰 Additional context used
🪛 GitHub Check: lint / lint
simd/ascii_amd64.go
[failure] 16-16:
confusing-naming: Method 'isASCII' differs only by capitalization to function 'IsASCII' in simd/ascii.go (revive)
simd/memchr_digit.go
[failure] 18-18:
confusing-naming: Method 'MemchrDigit' differs only by capitalization to function 'memchrDigit' in simd/memchr_digit_amd64.go (revive)
simd/memchr_class_amd64.go
[failure] 28-28:
confusing-naming: Method 'memchrNotWord' differs only by capitalization to function 'MemchrNotWord' in simd/memchr_class.go (revive)
[failure] 20-20:
confusing-naming: Method 'memchrWord' differs only by capitalization to function 'MemchrWord' in simd/memchr_class.go (revive)
simd/memchr.go
[failure] 50-50:
confusing-naming: Method 'MemchrPair' differs only by capitalization to function 'memchrPair' in simd/memchr_amd64.go (revive)
[failure] 35-35:
confusing-naming: Method 'Memchr3' differs only by capitalization to function 'memchr3' in simd/memchr_amd64.go (revive)
[failure] 25-25:
confusing-naming: Method 'Memchr2' differs only by capitalization to function 'memchr2' in simd/memchr_amd64.go (revive)
🔇 Additional comments (30)
ascii.go (1)
4-5: LGTM!Also applies to: 15-32
internal/unsafeconv/unsafeconv.go (1)
16-25: LGTM!search.go (1)
5-6: LGTM!Also applies to: 21-27, 72-78
search_test.go (1)
330-346: LGTM!simd/LICENSE (1)
1-22: LGTM!simd/simd_test.go (1)
1-572: LGTM!simd/fuzz_test.go (1)
1-117: LGTM!simd/bench_test.go (1)
1-217: LGTM!README.md (3)
132-136: 📐 Maintainability & Code Quality | ⚡ Quick win"See the simd section below for amd64 numbers" doesn't hold for IndexAny2/IndexAny3.
The amd64 benchmark block added at lines 552-632 (sourced from
simd/bench_test.go) only containsMemchr2/Memchr3/MemchrPair/MemchrDigit/MemchrNotWord/Memmem/IsASCIIrows — there are noBenchmark_IndexAny2/Benchmark_IndexAny3entries, since those top-level wrappers aren't benchmarked insimd/bench_test.go. The identical pointer forIsASCII(lines 184-186) is satisfied by that block; this one isn't. Either add amd64IndexAny2/IndexAny3benchmark rows (likely from a top-level bench file) or reword the note to point at the underlyingMemchr2/Memchr3/MemchrPairnumbers it actually dispatches to.
27-451: LGTM!
184-186: LGTM!Also applies to: 501-633
simd/simd.go (1)
26-38: LGTM!simd/memchr_other.go (1)
11-21: LGTM!simd/ascii_amd64.s (1)
33-114: LGTM!simd/memchr_amd64.go (1)
23-46: LGTM!simd/memchr_amd64.s (1)
35-412: LGTM!simd/memchr_class_amd64.s (1)
28-401: LGTM!simd/memchr_digit_amd64.go (1)
17-22: LGTM!simd/memchr_digit_amd64.s (1)
32-158: LGTM!simd/cpu_amd64.go (1)
1-14: LGTM!simd/cpu_other.go (1)
1-8: LGTM!simd/ascii.go (1)
1-99: LGTM!simd/ascii_other.go (1)
1-13: LGTM!simd/memchr.go (1)
1-170: LGTM!simd/memchr_class.go (1)
1-137: LGTM!simd/memchr_class_other.go (1)
1-18: LGTM!simd/memchr_digit.go (1)
1-66: LGTM!simd/memchr_digit_other.go (1)
1-13: LGTM!simd/memmem.go (1)
1-132: LGTM!simd/byte_frequencies.go (1)
1-96: LGTM!
The simd benchmark block has no IndexAny2/IndexAny3 rows; the amd64 numbers for those functions are the Memchr2/Memchr3 kernel rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
FirstNonASCII's overlapping-final-word hit and MemchrDigitAt's in-bounds miss were the only unexecuted statements; package simd is now at 100% statement coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.
| Benchmark suite | Current: 92b3894 | Previous: 6488cc3 | Ratio |
|---|---|---|---|
Benchmark_TrimSpace/fiber/empty (github.com/gofiber/utils/v2) |
0.6256 ns/op 0 B/op 0 allocs/op |
0.3128 ns/op 0 B/op 0 allocs/op |
2 |
Benchmark_TrimSpace/fiber/empty (github.com/gofiber/utils/v2) - ns/op |
0.6256 ns/op |
0.3128 ns/op |
2 |
Benchmark_IndexAny2/32B/swar (github.com/gofiber/utils/v2) - MB/s |
4869.29 MB/s |
3236.64 MB/s |
1.50 |
Benchmark_IndexAny2/64B/swar (github.com/gofiber/utils/v2) - MB/s |
8897.76 MB/s |
4022.3 MB/s |
2.21 |
Benchmark_IndexAny2/512B/swar (github.com/gofiber/utils/v2) - MB/s |
32081.86 MB/s |
4917.01 MB/s |
6.52 |
Benchmark_IndexAny3/32B/swar (github.com/gofiber/utils/v2) - MB/s |
4455.84 MB/s |
2776.28 MB/s |
1.60 |
Benchmark_IndexAny3/64B/swar (github.com/gofiber/utils/v2) - MB/s |
8199.53 MB/s |
3255.26 MB/s |
2.52 |
Benchmark_IndexAny3/512B/swar (github.com/gofiber/utils/v2) - MB/s |
29337.53 MB/s |
3778.63 MB/s |
7.76 |
Benchmark_IsASCII/512B/swar (github.com/gofiber/utils/v2) - MB/s |
37021.15 MB/s |
15793.8 MB/s |
2.34 |
This comment was automatically generated by workflow using github-action-benchmark.
|
Note on the failing 7 of the 8 flagged rows are MB/s (throughput) entries where the "Current" value is 1.5x-7.8x higher than master — e.g. The one genuine ns/op row, This alert will re-fire on every run of this PR as long as the speedups exist. A durable fix would live in the shared workflow ( Generated by Claude Code |
@ReneWerner87 Check this out, it found a bug in the benchmark tool 🤣 |
|
Ok sure |
|
About a year ago I prototyped something similar for Fiber utils ToLower (https://github.com/sixcolors/fiber_tolower_test/tree/for-gaby). Even a cheap branch to special-case short strings (HEAD/GET/etc.) regressed vs the simple path, and hand-rolling SIMD across ISAs didn’t feel worth the upkeep. Glad this lands the other way: one simd package, AVX2 only on amd64 for ≥ MinLen, SWAR elsewhere. That matches what I’d want here. Two open questions:
|
|
Good questions — data for both: 1. Sub-32B cost of the prologue Structurally, sub-32B inputs pay exactly one extra compare-and-branch on amd64: the gate is Measured ( So: a wash within noise on most rows, with worst observed swings of ±1ns at the 4-9ns scale — and the sign flips between rows and between runs, which points at code-layout/alignment effects rather than the branch itself (a predicted compare is ~0.3ns on these cores; some rows improved despite the added branch). For the short tokens Fiber hits most ( 2. Backend plan The dispatch seam is built for more backends without API churn: each function is Generated by Claude Code |
- SelectRareBytes: prefer a distinct second rare byte, so needles with a duplicated rarest byte (e.g. "zze") stay eligible for the paired prefilter instead of collapsing to Byte1 == Byte2; document that Index1/Index2 are first occurrences and that distinct values are guaranteed whenever the needle has two - Memmem: hand over to bytes.Index after exactly memmemMaxMisses failed verifications (one iteration earlier, matching the documented budget); drop the now-redundant Index1 != Index2 gate; describe the paired/single routing accurately in the docs - swar.ZeroLanes: document the every-true-zero-lane guarantee that memchrPairGeneric's mask AND relies on, and note the reliance there - tests: cover memmemPaired's miss-budget handover platform-independently (dense false-pair haystacks), pin memmemFallback's index translation, assert first-occurrence and distinct-value selection properties, and replace the tautological Test_Accelerated assertion with independent checks against x/sys/cpu and GOARCH - fuzz: seed FuzzMemmem so both prefilter paths reach memmemFallback (absent and found-by-fallback) from the seed corpus alone - bench: add Benchmark_Memmem_Prefilter, which forces the prefilter helpers at every size with the needle shapes Memmem actually routes to them, making the 128-byte routing constant measurable from the file - CI: set GOFIBER_SIMD_REQUIRE_AVX2 in the Test workflow so amd64 runners fail loudly if the kernels silently fall back - docs: refresh stale scalar-tail comments and provenance notes in the amd64 kernel files, align the package/README prose with the actual Memmem routing, and regenerate the amd64 benchmark block Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
- bench: Benchmark_Memmem_Prefilter's comment described benchData as uniform-random with a ~256-byte anchor recurrence; it is a repeating 'a'..'w' cycle whose 'q' anchor recurs every 23 bytes, so the miss budget engages from a few hundred bytes, not kilobyte sizes - memchrPairGeneric: fix the inverted ordering claim — after ANDing the ZeroLanes masks a flagged lane may be a false candidate strictly below the first true pair, which is exactly why candidates are re-verified - package doc: only Memmem's paired path runs on a package kernel (MemchrPair); the single-rare-byte path scans with bytes.IndexByte - memchr_amd64 header and provenance note: describe the pair kernel's two-load tail (byte1 window ending at len-offset, byte2 at the buffer end) instead of the single-vector phrasing that only fits memchr2/3; align the README phrase - tests: assert Byte2-minimality among values distinct from Byte1 in the SelectRareBytes property loop and add an "ezZq" promotion-path case, closing the gap where a frozen-Byte2 implementation passed the suite Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
…ackage doc
- tests: add a SelectRareBytes case whose Byte2 value repeats ("zzee"),
the only shape that distinguishes first- from last-occurrence Index2;
every existing needle's Byte2 value appeared exactly once, so a
last-occurrence regression previously passed the whole suite
- package doc: short single-valued needles take the single rare-byte
scan, not bytes.Index, and the paired path requires two distinct
values rather than exactly two — mirror Memmem's own doc wording
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Summary
Ports the
simdpackage from coregx/coregex (MIT, license preserved insimd/LICENSEand per-file headers) as a new exported subpackage, without taking coregex on as a dependency: AVX2 assembly kernels on amd64 processing 32 bytes per iteration, with portable fallbacks built on our ownswarpackage everywhere else. The top-levelIsASCII,IndexAny2, andIndexAny3dispatch into it automatically for inputs ofsimd.MinLen(32) bytes and up on amd64, via a zero-copybyteSeqview. The only new module dependency isgolang.org/x/sys(CPU feature detection).Fixes #234
New API (package
simd)Memchr2,Memchr3(multi-needle),MemchrPair(fixed-distance pair, substring prefilter),MemchrDigit/MemchrDigitAt,MemchrWord/MemchrNotWord,IsASCII,Memmem(rare-byte prefiltered substring search)FirstNonASCII,CountNonASCIIMemchrInTable,MemchrNotInTableSelectRareBytes,ByteRank,RareByteInfo,Accelerated(),MinLenThere is deliberately no single-needle
Memchr:bytes.IndexByteis already vector-accelerated by the Go runtime and beat the upstream kernel at every size.Adaptations from upstream
MOVD/MOVQwith VEX instructions, a ~150 ns/call stall on Intel (Memchr2512B: 209 ns → 16 ns after switching toVMOVD/VMOVQ).Memmem's worst case: the prefilter loops bail tobytes.Indexafter 16 failed candidate verifications, so the adversarial shape (rare byte everywhere + long almost-matching needle) stays within ~2% of stdlib instead of degrading to O(n·m) (measured 47 ms → 1.67 ms vs stdlib's 1.64 ms on a 1 MiB haystack). Below its measured 128-byte crossoverMemmemdelegates tobytes.Indexoutright.swar: one source of truth for the bit tricks; this also gives the digit/word-class scans andFirstNonASCII/CountNonASCIIword-wide fallbacks on non-amd64 (arm64) instead of byte-at-a-time loops.ByteRankso callers can't mutate the ranking the prefilter depends on.Benchmarks
Environment: linux/amd64, Intel Xeon @ 2.10 GHz (AVX2), Go 1.25. Full tables in the README; per-commit charts will pick the new benchmarks up.
Top-level functions vs master (
benchstat -count=10, sec/op; sub-32B inputs keep the SWAR path and are unchanged within noise):Package
simdvs the idiomatic alternative (stdlib where one exists, scalar loop otherwise; worst-case full-scan misses, ns/op):Memchr2vsbytes.ContainsAnyMemchr3vsbytes.ContainsAnyMemchrPairvs scalarMemchrDigitvs scalarMemchrNotWordvs scalarIsASCIIvs SWARMemmemvsbytes.Index— the 96B/128B rows bracket the crossover encoded inmemmemMinHaystack, and the adversarial row pins the miss-budget fallback:Memmembytes.IndexTesting
Memmemprefilter internals directly (so they stay covered on runners without AVX2); ~3M execs clean.GOFIBER_SIMD_REQUIRE_AVX2=1makesTest_Acceleratedfail if the assembly kernels are not actually active, guarding against silently fallback-only CI.-race -shuffle=on; builds verified on amd64, arm64, 386, s390x, and wasm; golangci-lint clean.🤖 Generated with Claude Code
https://claude.ai/code/session_017QqEx79JhskHN7sRz5Pj5R
Summary by CodeRabbit
Summary by CodeRabbit
New Features
Documentation
Tests