⚡ feat: Add zero-allocation HTTP date, URL escaping, JSON string, IP parsing, and header key helpers - #238
Conversation
Three new zero-allocation helper groups for Fiber's per-request paths, each pinned to its stdlib counterpart by unit and fuzz tests: - AppendHTTPDate/FormatHTTPDate write net/http.TimeFormat dates by overwriting a fixed template (3.4x vs time.AppendFormat), and ParseHTTPDate parses canonical RFC 9110 dates on a strict scalar fast path (7.8x vs time.Parse) while delegating legacy forms to time.Parse with net/http.ParseTime semantics. - AppendQueryEscape/AppendPathEscape and the matching Unescape helpers reproduce net/url byte-for-byte (including EscapeError values) as single-pass append-style functions; unescaping skips between escape sites with IndexAny2/bytes.IndexByte and supports in-place decoding (1.3x-5.5x, no allocations). - AppendHexEncode expands eight input bytes per SWAR word pair into lowercase hex (1.1x-1.4x vs encoding/hex.AppendEncode). A SWAR hex decoder measured slower than encoding/hex's table loop on amd64, so decoding deliberately stays with the stdlib. Speedups are linux/amd64 measurements; the README records the new benchmark rows in a separately labeled block like the simd package. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj
Hex encoding stays with the stdlib (hex.AppendEncode) alongside UUID and base64; the URL unescape helpers keep their own internal hex-digit table, which is not public API. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj
Three more zero-allocation helper groups for Fiber's per-request paths, each pinned to its stdlib counterpart by unit and fuzz tests: - AppendJSONString writes a double-quoted JSON string byte-identical to encoding/json.Marshal of the same value (default HTML escaping, \ufffd replacement, U+2028/U+2029 escapes) using a SWAR first-match scan over clean spans — 1.9x-6.9x with zero allocations versus Marshal's reflection path. - ParseIPv4/ParseIPv6 parse into netip.Addr, accepting exactly the strings netip.ParseAddr accepts per family (1.4x on IPv4, 1.25x-1.35x on IPv6), reporting failure via bool instead of a constructed error and taking []byte as well as string input. - CanonicalHeaderKey mirrors net/http.CanonicalHeaderKey byte for byte, validating already-canonical keys in one table pass and returning them as-is; rewritten keys cost one allocation (1.1x-1.4x). Adds an unsafeconv.Seq view helper as the zero-copy inverse of unsafeconv.Bytes. Speedups are linux/amd64 measurements; README rows recorded in the amd64-labeled block alongside the HTTP date and URL helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj
Bring every function added on this branch to 100% statement coverage and pin the stdlib-parity contracts with enumerated input spaces instead of hand-picked cases: - ParseHTTPDate: per-position byte mutations and truncations of the canonical form, all twelve months and seven weekday names, and a calendar sweep (day 0/28-32/99 across leap, century, and year-0000 boundaries; clock-field limits), all checked against the net/http.ParseTime reference. - URL escaping: every byte value in each position of a %XX escape, long inputs that engage the vectorized scans with escapes at boundary offsets, late-error dst preservation, and the full byte alphabet through both escape modes. - AppendJSONString: every special byte and multi-byte sequence swept across SWAR word-boundary offsets and string tails, plus malformed UTF-8 (overlong forms, surrogate halves, out-of-range and truncated sequences), all against encoding/json.Marshal. - ParseIPv4/ParseIPv6: per-position mutations and truncations of seven representative addresses plus targeted invalid forms (covering the one previously unhit branch: a group followed by a non-separator byte), all against netip.ParseAddr. - CanonicalHeaderKey: all 256 bytes at every position of eight key shapes against net/http.CanonicalHeaderKey, plus input-immutability. - internal/unsafeconv: direct round-trip and aliasing tests for UnsafeString/UnsafeBytes/Bytes/Seq (package now at 100%). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #238 +/- ##
=========================================
Coverage ? 92.71%
=========================================
Files ? 33
Lines ? 2140
Branches ? 0
=========================================
Hits ? 1984
Misses ? 135
Partials ? 21
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 runs a newer golangci-lint than the Makefile pin and flagged four issues: replace the twin-result table builder with a parameterized single-table builder (confusing-results), replace the bool control parameters in appendEscape/appendUnescape with an escapeMode enum (flag-parameter), and store IPv6 groups via binary.BigEndian.PutUint16 instead of indexed writes gosec cannot prove in range (G602). No behavior change; benchmarks unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj
|
Warning Review limit reached
Next review available in: 39 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 Plus Run ID: 📒 Files selected for processing (15)
✨ 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.
⚠️ 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: 7f5ce13 | Previous: ab61971 | Ratio |
|---|---|---|---|
Benchmark_AddTrailingSlashBytes/empty (github.com/gofiber/utils/v2) |
1.409 ns/op 0 B/op 0 allocs/op |
0.5465 ns/op 0 B/op 0 allocs/op |
2.58 |
Benchmark_AddTrailingSlashBytes/empty (github.com/gofiber/utils/v2) - ns/op |
1.409 ns/op |
0.5465 ns/op |
2.58 |
Benchmark_AddTrailingSlashBytes/slash-only (github.com/gofiber/utils/v2) |
1.763 ns/op 0 B/op 0 allocs/op |
1.109 ns/op 0 B/op 0 allocs/op |
1.59 |
Benchmark_AddTrailingSlashBytes/slash-only (github.com/gofiber/utils/v2) - ns/op |
1.763 ns/op |
1.109 ns/op |
1.59 |
Benchmark_AddTrailingSlashBytes/short-with-slash (github.com/gofiber/utils/v2) |
1.762 ns/op 0 B/op 0 allocs/op |
1.096 ns/op 0 B/op 0 allocs/op |
1.61 |
Benchmark_AddTrailingSlashBytes/short-with-slash (github.com/gofiber/utils/v2) - ns/op |
1.762 ns/op |
1.096 ns/op |
1.61 |
Benchmark_AddTrailingSlashBytes/path-with-slash (github.com/gofiber/utils/v2) |
1.765 ns/op 0 B/op 0 allocs/op |
1.097 ns/op 0 B/op 0 allocs/op |
1.61 |
Benchmark_AddTrailingSlashBytes/path-with-slash (github.com/gofiber/utils/v2) - ns/op |
1.765 ns/op |
1.097 ns/op |
1.61 |
Benchmark_TrimLeftBytes/fiber (github.com/gofiber/utils/v2) |
3.24 ns/op 0 B/op 0 allocs/op |
1.819 ns/op 0 B/op 0 allocs/op |
1.78 |
Benchmark_TrimLeftBytes/fiber (github.com/gofiber/utils/v2) - ns/op |
3.24 ns/op |
1.819 ns/op |
1.78 |
Benchmark_TrimSpace/fiber/empty (github.com/gofiber/utils/v2) |
0.7107 ns/op 0 B/op 0 allocs/op |
0.276 ns/op 0 B/op 0 allocs/op |
2.57 |
Benchmark_TrimSpace/fiber/empty (github.com/gofiber/utils/v2) - ns/op |
0.7107 ns/op |
0.276 ns/op |
2.57 |
Benchmark_TrimSpace/fiber/spaces (github.com/gofiber/utils/v2) |
4.227 ns/op 709.76 MB/s 0 B/op 0 allocs/op |
2.271 ns/op 1320.91 MB/s 0 B/op 0 allocs/op |
1.86 |
Benchmark_TrimSpace/fiber/spaces (github.com/gofiber/utils/v2) - ns/op |
4.227 ns/op |
2.271 ns/op |
1.86 |
Benchmark_TrimSpaceBytes/fiber/ascii-word (github.com/gofiber/utils/v2) |
4.581 ns/op 1964.65 MB/s 0 B/op 0 allocs/op |
2.74 ns/op 3284.62 MB/s 0 B/op 0 allocs/op |
1.67 |
Benchmark_TrimSpaceBytes/fiber/ascii-word (github.com/gofiber/utils/v2) - ns/op |
4.581 ns/op |
2.74 ns/op |
1.67 |
Benchmark_TrimSpaceBytes/fiber/content-type (github.com/gofiber/utils/v2) |
4.204 ns/op 4756.84 MB/s 0 B/op 0 allocs/op |
2.738 ns/op 7305.47 MB/s 0 B/op 0 allocs/op |
1.54 |
Benchmark_TrimSpaceBytes/fiber/content-type (github.com/gofiber/utils/v2) - ns/op |
4.204 ns/op |
2.738 ns/op |
1.54 |
Benchmark_TrimSpaceBytes/fiber/query-params (github.com/gofiber/utils/v2) |
4.229 ns/op 4729.21 MB/s 0 B/op 0 allocs/op |
2.737 ns/op 7307.90 MB/s 0 B/op 0 allocs/op |
1.55 |
Benchmark_TrimSpaceBytes/fiber/query-params (github.com/gofiber/utils/v2) - ns/op |
4.229 ns/op |
2.737 ns/op |
1.55 |
Benchmark_TrimSpaceBytes/fiber/mixed-whitespace (github.com/gofiber/utils/v2) |
5.646 ns/op 3011.16 MB/s 0 B/op 0 allocs/op |
3.598 ns/op 4725.03 MB/s 0 B/op 0 allocs/op |
1.57 |
Benchmark_TrimSpaceBytes/fiber/mixed-whitespace (github.com/gofiber/utils/v2) - ns/op |
5.646 ns/op |
3.598 ns/op |
1.57 |
Benchmark_ToString/uint16 (github.com/gofiber/utils/v2) |
4.602 ns/op 0 B/op 0 allocs/op |
2.527 ns/op 0 B/op 0 allocs/op |
1.82 |
Benchmark_ToString/uint16 (github.com/gofiber/utils/v2) - ns/op |
4.602 ns/op |
2.527 ns/op |
1.82 |
Benchmark_ToString/uint32 (github.com/gofiber/utils/v2) |
4.225 ns/op 0 B/op 0 allocs/op |
2.557 ns/op 0 B/op 0 allocs/op |
1.65 |
Benchmark_ToString/uint32 (github.com/gofiber/utils/v2) - ns/op |
4.225 ns/op |
2.557 ns/op |
1.65 |
Benchmark_ToString/utils.MyStringer (github.com/gofiber/utils/v2) |
5.989 ns/op 0 B/op 0 allocs/op |
3.826 ns/op 0 B/op 0 allocs/op |
1.57 |
Benchmark_ToString/utils.MyStringer (github.com/gofiber/utils/v2) - ns/op |
5.989 ns/op |
3.826 ns/op |
1.57 |
Benchmark_ToString_concurrency/uint16 (github.com/gofiber/utils/v2) |
2.131 ns/op 0 B/op 0 allocs/op |
1.24 ns/op 0 B/op 0 allocs/op |
1.72 |
Benchmark_ToString_concurrency/uint16 (github.com/gofiber/utils/v2) - ns/op |
2.131 ns/op |
1.24 ns/op |
1.72 |
Benchmark_ToString_concurrency/uint32 (github.com/gofiber/utils/v2) |
2.137 ns/op 0 B/op 0 allocs/op |
1.232 ns/op 0 B/op 0 allocs/op |
1.73 |
Benchmark_ToString_concurrency/uint32 (github.com/gofiber/utils/v2) - ns/op |
2.137 ns/op |
1.232 ns/op |
1.73 |
Benchmark_FormatUint/small/fiber (github.com/gofiber/utils/v2) |
2.46 ns/op 0 B/op 0 allocs/op |
1.637 ns/op 0 B/op 0 allocs/op |
1.50 |
Benchmark_FormatUint/small/fiber (github.com/gofiber/utils/v2) - ns/op |
2.46 ns/op |
1.637 ns/op |
1.50 |
Benchmark_ParseInt16/fiber (github.com/gofiber/utils/v2) |
8.806 ns/op 0 B/op 0 allocs/op |
5.784 ns/op 0 B/op 0 allocs/op |
1.52 |
Benchmark_ParseInt16/fiber (github.com/gofiber/utils/v2) - ns/op |
8.806 ns/op |
5.784 ns/op |
1.52 |
Benchmark_ParseInt16/fiber_bytes (github.com/gofiber/utils/v2) |
8.993 ns/op 0 B/op 0 allocs/op |
5.965 ns/op 0 B/op 0 allocs/op |
1.51 |
Benchmark_ParseInt16/fiber_bytes (github.com/gofiber/utils/v2) - ns/op |
8.993 ns/op |
5.965 ns/op |
1.51 |
Benchmark_IndexAny2/64B/swar (github.com/gofiber/utils/v2) - MB/s |
9078.25 MB/s |
4985.85 MB/s |
1.82 |
Benchmark_IndexAny2/512B/swar (github.com/gofiber/utils/v2) - MB/s |
29045.81 MB/s |
5731.49 MB/s |
5.07 |
Benchmark_IndexAny3/64B/swar (github.com/gofiber/utils/v2) - MB/s |
7874.52 MB/s |
3971.05 MB/s |
1.98 |
Benchmark_IndexAny3/512B/swar (github.com/gofiber/utils/v2) - MB/s |
26904.54 MB/s |
4512.22 MB/s |
5.96 |
Benchmark_IsASCII/32B/swar (github.com/gofiber/utils/v2) |
6.956 ns/op 4600.33 MB/s 0 B/op 0 allocs/op |
3.597 ns/op 8895.25 MB/s 0 B/op 0 allocs/op |
1.93 |
Benchmark_IsASCII/32B/swar (github.com/gofiber/utils/v2) - ns/op |
6.956 ns/op |
3.597 ns/op |
1.93 |
Benchmark_AddTrailingSlashString/empty (github.com/gofiber/utils/v2) |
0.704 ns/op 0 B/op 0 allocs/op |
0.2736 ns/op 0 B/op 0 allocs/op |
2.57 |
Benchmark_AddTrailingSlashString/empty (github.com/gofiber/utils/v2) - ns/op |
0.704 ns/op |
0.2736 ns/op |
2.57 |
Benchmark_AddTrailingSlashString/slash-only (github.com/gofiber/utils/v2) |
0.7063 ns/op 0 B/op 0 allocs/op |
0.4095 ns/op 0 B/op 0 allocs/op |
1.72 |
Benchmark_AddTrailingSlashString/slash-only (github.com/gofiber/utils/v2) - ns/op |
0.7063 ns/op |
0.4095 ns/op |
1.72 |
Benchmark_AddTrailingSlashString/short-with-slash (github.com/gofiber/utils/v2) |
0.7037 ns/op 0 B/op 0 allocs/op |
0.4095 ns/op 0 B/op 0 allocs/op |
1.72 |
Benchmark_AddTrailingSlashString/short-with-slash (github.com/gofiber/utils/v2) - ns/op |
0.7037 ns/op |
0.4095 ns/op |
1.72 |
Benchmark_AddTrailingSlashString/path-with-slash (github.com/gofiber/utils/v2) |
0.7043 ns/op 0 B/op 0 allocs/op |
0.4101 ns/op 0 B/op 0 allocs/op |
1.72 |
Benchmark_AddTrailingSlashString/path-with-slash (github.com/gofiber/utils/v2) - ns/op |
0.7043 ns/op |
0.4101 ns/op |
1.72 |
Benchmark_ToLower/large-lower/default (github.com/gofiber/utils/v2/strings) |
63.46 ns/op 0 B/op 0 allocs/op |
41.46 ns/op 0 B/op 0 allocs/op |
1.53 |
Benchmark_ToLower/large-lower/default (github.com/gofiber/utils/v2/strings) - ns/op |
63.46 ns/op |
41.46 ns/op |
1.53 |
This comment was automatically generated by workflow using github-action-benchmark.
Code review found two contract gaps: AppendJSONString omitted the dst-must-not-alias-s rule its sibling Append*Escape helpers document (the output is longer than the input, so an aliased dst overwrites bytes before they are read), and the unescape helpers' in-place mode did not state that a failed decode has already overwritten the input prefix with decoded bytes. Documentation only; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj
|
The
The alert will likely recur on future pushes until a master benchmark run refreshes the baseline past #237. Generated by Claude Code |
|
the date parse stuff is similar to my change in fasthttp |
|
Indeed, same shape as your fasthttp change: a strict 29-byte RFC1123-GMT fast path with fixed separator offsets, packed 3-byte weekday/month matching, and a stdlib fallback. A few deltas in this version, for comparison:
If it's useful upstream, fasthttp could converge on this helper once it lands here. Generated by Claude Code |
|
The benchmark alert fired again on
Nothing in this PR can turn this check green deterministically; a threshold above 1.5 (or ignoring sub-ns rows) in the shared benchmark workflow would be the structural fix. I won't re-post on further recurrences of this alert. Generated by Claude Code |
Summary
Five new zero-allocation helper groups for Fiber's per-request paths. Each one is pinned byte-for-byte to its stdlib counterpart by unit tests, differential edge-case suites, and fuzz targets, and every new function is at 100% statement coverage.
HTTP dates (
httpdate.go)AppendHTTPDate/FormatHTTPDatewritenet/http.TimeFormatdates by overwriting a fixed 29-byte template instead of walking a layout string — 3.4x vstime.AppendFormat, 0 allocs.ParseHTTPDateparses canonical RFC 9110 dates on a strict scalar fast path — 7.8x vstime.Parse— and delegates everything else (RFC 850, asctime, unusual casing, non-GMT zones, padding) totime.Parsewithnet/http.ParseTimesemantics, including its errors.URL escaping (
url.go)AppendQueryEscape/AppendPathEscapeand the matchingUnescapehelpers reproducenet/urlexactly (includingurl.EscapeErrorvalues) as single-pass append-style functions.IndexAny2,bytes.IndexByte) and supports in-place decoding (dst = s[:0]) — 1.3x–5.5x, 0 allocs.JSON string escaping (
jsonstring.go)AppendJSONStringis byte-identical toencoding/json.Marshalof a string value (default HTML escaping,�replacement, U+2028/U+2029 escapes) using a SWAR first-match scan over clean spans — 1.9x–6.9x, 0 allocs vs Marshal's reflection path.IP parsing (
ipparse.go)ParseIPv4/ParseIPv6parse intonetip.Addr, accepting exactly the per-family stringsnetip.ParseAddraccepts (pinned by fuzzing), reporting failure viaboolinstead of a constructed error, and taking[]byteas well asstring— 1.4x on IPv4, 1.25x–1.35x on IPv6.Header key canonicalization (
headerkey.go)CanonicalHeaderKeymirrorsnet/http.CanonicalHeaderKeyincluding its return-unchanged guard for invalid keys; already-canonical keys are validated in one table pass and returned as-is with zero allocations — 1.1x–1.4x. Addsinternal/unsafeconv.Seqas the zero-copy inverse ofunsafeconv.Bytes.Deliberately left to the stdlib
Hex encode/decode, base64, and UUID stay with
encoding/hex,encoding/base64, andgoogle/uuid. A SWAR hex encoder/decoder was built and benchmarked during development but removed: the decoder measured slower thanencoding/hex's table loop on amd64, and the maintainers prefer stdlib for these.Testing
time.Parse/net/http.ParseTime,net/url,encoding/json.Marshal,netip.ParseAddr, andhttp.CanonicalHeaderKey(~4M executions locally, zero divergence).edgecase_test.go): per-position byte mutations and truncations, calendar boundaries (leap years, century rule, year 0000, clock limits), every byte value in each%XXescape position, malformed UTF-8 (overlong forms, surrogate halves, truncated sequences) swept across SWAR word-boundary offsets, and all 256 bytes at every position of representative header keys.internal/unsafeconv(new test file).-race -shuffle=on; golangci-lint clean on all new files.Benchmarks
Measured on linux/amd64 (Intel Xeon @ 2.80GHz); recorded in the README's amd64-labeled block alongside the
simdnumbers, joining the arm64 catalog on its next regeneration. Highlights (fibervsdefault, ns/op):🤖 Generated with Claude Code
https://claude.ai/code/session_01H8HVkHMPXuCHfz7W7fAaEj