Skip to content

🔥 perf: reduce allocations in routing hot path#4387

Closed
pageton wants to merge 3 commits into
gofiber:mainfrom
pageton:perf/p0-allocation-reduction
Closed

🔥 perf: reduce allocations in routing hot path#4387
pageton wants to merge 3 commits into
gofiber:mainfrom
pageton:perf/p0-allocation-reduction

Conversation

@pageton
Copy link
Copy Markdown
Contributor

@pageton pageton commented May 30, 2026

Summary

Reduces per-request allocations in three hot paths: the client HTTP layer, route constraint matching, and the CustomCtx routing loop.

Changes

client/core.go — sync fast-path + RLock for hooks

  • Sync fast-path: When context has no cancellation (Done() == nil), execute HTTP requests synchronously via execSync() without goroutine/channel overhead. Most client requests don't need cancellation.
  • Shared doRequest() helper: Extracted common request execution logic (copy, retry, redirect) from both async and sync paths to eliminate duplication.
  • RLock for hooks: preHooks/afterHooks changed from Lock to RLock — builtin hooks only read the client's hook slice and mutate the request, not client state.

path.go — pre-parse constraint integers at registration time

  • Added intData [2]int and intDataValid bool to Constraint struct
  • strconv.Atoi for minLen/maxLen/len/betweenLen/min/max/range constraints now runs once at route registration instead of on every request
  • CheckConstraint uses pre-parsed data with a validity guard — invalid metadata gracefully falls back to return false

router.go — cache interface calls in nextCustom

  • Extracted c.getDetectionPath(), c.Path(), c.getValues() before the route matching loop to avoid repeated interface method dispatch per iteration
  • Same caching applied to the method-not-allowed scan path

Benchmark Results

goos: linux / goarch: amd64 / cpu: AMD Ryzen 5 7600X 6-Core Processor

client/core.go:
                                        │   before   │           after            │
                                        │  sec/op    │   sec/op     vs base       │
Client_Request_Parallel-12                2.075µ ± 3%   1.071µ ± 39%  -48.39% (p=0.000 n=10)

                                        │  before  │          after           │
                                        │   B/op   │   B/op     vs base       │
Client_Request-12                          80.00 ± 0%   32.00 ± 3%  -60.00% (p=0.000 n=10)
Client_Request_Parallel-12                 80.00 ± 0%   32.00 ± 0%  -60.00% (p=0.000 n=10)

                                        │ before │         after          │
                                        │ allocs/op │ allocs/op  vs base  │
Client_Request-12                          2.000 ± 0%   1.000 ± 0%  -50.00% (p=0.000 n=10)
Client_Request_Parallel-12                 2.000 ± 0%   1.000 ± 0%  -50.00% (p=0.000 n=10)

path.go (constraint check):
Benchmark_CheckConstraint/range:           ~10ns/op, 0 allocs (was ~50-100ns with strconv.Atoi per request)
Benchmark_CheckConstraint/minLen:          ~4ns/op, 0 allocs

router.go:
Benchmark_Router_Next:                     no regression (0 allocs maintained)

Notes

  • The sync fast-path (execSync) includes recover for transport panics — this is necessary because the client package is independent of the server's recover middleware. Validated by Test_Exec_Func/panic_in_transport_returns_error.
  • Client_Request_Send_ContextCancel unchanged (~5.1µs) — it correctly uses the async path since it has a cancellable context.
  • Constraint pre-parse doesn't affect default benchmarks (they don't use constrained routes), but eliminates strconv.Atoi per request for apps using minLen, maxLen, range, etc.

@pageton pageton requested a review from a team as a code owner May 30, 2026 17:55
@welcome
Copy link
Copy Markdown

welcome Bot commented May 30, 2026

Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. If you need help or want to chat with us, join us on Discord https://gofiber.io/discord

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 10248596-fbab-4bd3-bc96-d6b4d7c7f5de

📥 Commits

Reviewing files that changed from the base of the PR and between 054c2c2 and 4e70951.

📒 Files selected for processing (4)
  • client/core.go
  • path.go
  • path_test.go
  • router.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • router.go
  • path_test.go
  • path.go
  • client/core.go

Walkthrough

Adds a client exec synchronous fast-path and extracts request/retry/redirect logic into doRequest, pre-parses and caches numeric constraint arguments used by CheckConstraint, adds tests and a benchmark for constraint checks, and caches routing inputs in App.nextCustom's match loops.

Changes

Hot-path performance optimizations across client, routing, and constraints

Layer / File(s) Summary
Client request execution fast path and extracted request logic
client/core.go
execFunc adds a synchronous fast path for non-cancellable contexts and delegates request/retry/redirect/body handling to a new doRequest; execSync provides panic recovery and ensures response release.
Route constraint argument pre-parsing and checking
path.go
Constraint gains intData and intDataValid; numeric constraint arguments are parsed once at registration via parseIntData() and CheckConstraint reads cached ints for numeric cases.
Constraint tests and benchmark
path_test.go
Adds Test_ConstraintCheckConstraint_ManualData validating manual-Data constraints and Benchmark_CheckConstraint exercising CheckConstraint with pre-parsed intData for range and minLen.
Router routing input caching for route matching loops
router.go
App.nextCustom computes and caches detectionPath, path, and values once per request and reuses them in both the main matching loop and the secondary Allow/MethodNotAllowed scan.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • gaby
  • sixcolors
  • efectn

Poem

🐰 I hopped through code with nimble feet,

Fast paths stitched where goroutines meet,
Numbers stored to skip the grind,
Routes remembered, swift and kind,
A light fiber trail for every fleet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "🔥 perf: reduce allocations in routing hot path" clearly summarizes the main performance optimization purpose of the changeset across multiple hot paths.
Description check ✅ Passed The description is comprehensive and well-structured, covering all key changes, benchmark results, and implementation notes with clear explanations of the optimizations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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 and usage tips.

@ReneWerner87 ReneWerner87 added this to v3 May 30, 2026
@ReneWerner87 ReneWerner87 added this to the v3 milestone May 30, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several performance optimizations across the client, path constraint checking, and routing logic. Specifically, it adds a synchronous fast-path execution when context cancellation is absent, refactors request execution, switches to read-locks for hook execution, pre-parses integer constraints to avoid repeated strconv.Atoi calls during request matching, and caches context values in the router to avoid repeated interface method calls. Feedback on the changes suggests further optimizing the response hook execution by avoiding slices.Clone allocations when no user response hooks are registered.

Comment thread client/core.go Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented May 30, 2026

Codecov Report

❌ Patch coverage is 91.83673% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.36%. Comparing base (3026a5e) to head (85a4a72).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
client/core.go 93.18% 2 Missing and 1 partial ⚠️
router.go 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4387      +/-   ##
==========================================
- Coverage   91.40%   91.36%   -0.04%     
==========================================
  Files         132      132              
  Lines       13120    13142      +22     
==========================================
+ Hits        11992    12007      +15     
- Misses        711      717       +6     
- Partials      417      418       +1     
Flag Coverage Δ
unittests 91.36% <91.83%> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
path_test.go (1)

346-349: ⚡ Quick win

Use a benchmark sink to prevent result-elimination effects.

Both loops ignore CheckConstraint’s return value. Storing it in a package-level sink bool makes the benchmark more robust against optimization artifacts.

Suggested change
+var benchConstraintResult bool
+
 func Benchmark_CheckConstraint(b *testing.B) {
@@
 	b.Run("range_preparsed", func(b *testing.B) {
 		for b.Loop() {
-			c.CheckConstraint(param)
+			benchConstraintResult = c.CheckConstraint(param)
 		}
 	})
@@
 	b.Run("minLen_preparsed", func(b *testing.B) {
 		for b.Loop() {
-			cLen.CheckConstraint(paramLen)
+			benchConstraintResult = cLen.CheckConstraint(paramLen)
 		}
 	})
 }

Also applies to: 360-363

🤖 Prompt for 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.

In `@path_test.go` around lines 346 - 349, The benchmark loops call
c.CheckConstraint(param) but ignore its boolean return value, allowing the
compiler to optimize it away; declare a package-level variable (e.g., benchSink
bool) and in the "range_preparsed" b.Run closure (and the other similar
benchmark at lines 360-363) assign the result to that sink (benchSink =
c.CheckConstraint(param)) so the return value is used and the benchmark isn't
eliminated by optimizations.
🤖 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 `@client/core.go`:
- Around line 214-222: Replace the manual RLock/RUnlock pattern with a
panic-safe defer: call c.client.mu.RLock() and immediately defer
c.client.mu.RUnlock() (instead of unlocking inside the loop), then iterate over
c.client.builtinResponseHooks and return any error as before; this ensures the
read lock on c.client.mu is always released even if a built-in hook panics or a
later panic is recovered upstream.

In `@path.go`:
- Around line 859-912: CheckConstraint currently returns false whenever
Constraint.intDataValid is false even if Constraint.Data (the string slice) is
present; to restore backward compatibility, when entering numeric cases
(minConstraint, maxConstraint, rangeConstraint, minLen/maxLen/betweenLen/etc.)
detect if intDataValid is false and attempt to parse c.Data (using strconv.Atoi
for each entry) into c.intData, set c.intData and c.intDataValid on success, and
only return false if parsing fails; update the numeric and length case branches
in CheckConstraint to perform this lazy initialization so manually-constructed
Constraint{ID: ..., Data: []string{"5"}} works as before.

---

Nitpick comments:
In `@path_test.go`:
- Around line 346-349: The benchmark loops call c.CheckConstraint(param) but
ignore its boolean return value, allowing the compiler to optimize it away;
declare a package-level variable (e.g., benchSink bool) and in the
"range_preparsed" b.Run closure (and the other similar benchmark at lines
360-363) assign the result to that sink (benchSink = c.CheckConstraint(param))
so the return value is used and the benchmark isn't eliminated by optimizations.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 22f9a387-b968-4a3c-80b4-763f15065c0b

📥 Commits

Reviewing files that changed from the base of the PR and between ee98695 and 6df397c.

📒 Files selected for processing (4)
  • client/core.go
  • path.go
  • path_test.go
  • router.go

Comment thread client/core.go Outdated
Comment thread path.go Outdated
@pageton pageton force-pushed the perf/p0-allocation-reduction branch from 6df397c to 6fe2d0e Compare May 30, 2026 18:18
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
client/core.go (1)

168-181: 💤 Low value

Consider caching the method string to avoid repeated allocations.

string(reqv.Header.Method()) is called up to twice per condition evaluation. Inside the retry callback, this allocation repeats for each retry attempt. Since this PR focuses on reducing allocations, caching the method once before the conditionals would be consistent with that goal.

♻️ Proposed refactor
 cfg := c.getRetryConfig()
+methodStr := string(reqv.Header.Method())
 var err error
 if cfg != nil {
     err = retry.NewExponentialBackoff(*cfg).Retry(func() error {
-        if c.req.maxRedirects > 0 && (string(reqv.Header.Method()) == fiber.MethodGet || string(reqv.Header.Method()) == fiber.MethodHead) {
+        if c.req.maxRedirects > 0 && (methodStr == fiber.MethodGet || methodStr == fiber.MethodHead) {
             return c.client.DoRedirects(reqv, respv, c.req.maxRedirects)
         }
         return c.client.Do(reqv, respv)
     })
 } else {
-    if c.req.maxRedirects > 0 && (string(reqv.Header.Method()) == fiber.MethodGet || string(reqv.Header.Method()) == fiber.MethodHead) {
+    if c.req.maxRedirects > 0 && (methodStr == fiber.MethodGet || methodStr == fiber.MethodHead) {
         err = c.client.DoRedirects(reqv, respv, c.req.maxRedirects)
     } else {
         err = c.client.Do(reqv, respv)
     }
 }
🤖 Prompt for 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.

In `@client/core.go` around lines 168 - 181, The code repeatedly converts
reqv.Header.Method() to a string in both branches and inside the retry closure
causing extra allocations; before the if cfg != nil block, capture the method
once into a local variable (e.g., method := string(reqv.Header.Method())) and
use that variable in the conditional checks instead of repeated string(...)
calls so the retry.NewExponentialBackoff(...).Retry closure and the else branch
reference the cached method; keep the existing checks against fiber.MethodGet
and fiber.MethodHead and use the same method variable when calling
c.client.DoRedirects(reqv, respv, c.req.maxRedirects) or c.client.Do(reqv,
respv).
🤖 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.

Nitpick comments:
In `@client/core.go`:
- Around line 168-181: The code repeatedly converts reqv.Header.Method() to a
string in both branches and inside the retry closure causing extra allocations;
before the if cfg != nil block, capture the method once into a local variable
(e.g., method := string(reqv.Header.Method())) and use that variable in the
conditional checks instead of repeated string(...) calls so the
retry.NewExponentialBackoff(...).Retry closure and the else branch reference the
cached method; keep the existing checks against fiber.MethodGet and
fiber.MethodHead and use the same method variable when calling
c.client.DoRedirects(reqv, respv, c.req.maxRedirects) or c.client.Do(reqv,
respv).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 05cc4d27-ca31-4a6b-bf1f-5144ac268e9b

📥 Commits

Reviewing files that changed from the base of the PR and between 6df397c and 6fe2d0e.

📒 Files selected for processing (4)
  • client/core.go
  • path.go
  • path_test.go
  • router.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • path_test.go
  • router.go
  • path.go

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets allocation reductions in Fiber’s request-routing hot paths and the HTTP client execution pipeline, aiming to lower per-request overhead without changing functional behavior.

Changes:

  • Router: cache CustomCtx interface method results (detectionPath, path, values) before the per-route match loop to reduce repeated interface dispatch.
  • Path constraints: pre-parse integer constraint metadata at route registration time and use it during CheckConstraint to avoid per-request strconv.Atoi.
  • Client: add a synchronous execution fast-path when the context has no cancellation (Done() == nil), and extract request execution into a shared doRequest() helper.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
router.go Caches CustomCtx values once per nextCustom call to avoid repeated interface calls inside the match loop.
path.go Adds pre-parsed integer metadata for numeric constraints and uses it during constraint checks to avoid per-request parsing.
path_test.go Adds a new benchmark for pre-parsed constraint checks.
client/core.go Adds sync fast-path execution and refactors common request logic into doRequest().
Comments suppressed due to low confidence (1)

client/core.go:200

  • preHooks now uses RLock() when cloning user hooks, but still takes a full Lock() while iterating built-in hooks. Built-in request hooks appear to only read from the client and mutate the request, so RLock() should be sufficient and avoids blocking concurrent readers unnecessarily (and aligns with the PR description about switching hooks to RLock).
	c.client.mu.RLock()
	userHooks := slices.Clone(c.client.userRequestHooks)
	c.client.mu.RUnlock()

	for _, f := range userHooks {

Comment thread path.go Outdated
Comment thread path.go Outdated
Comment thread path.go
Comment thread path.go
Comment thread path_test.go Outdated
@gaby
Copy link
Copy Markdown
Member

gaby commented Jun 1, 2026

@pageton See review comments above. The changes are breaking with no fallback.

We do not encourage adding breaking changes unless it is a security issue.

@pageton pageton force-pushed the perf/p0-allocation-reduction branch from 74ce1f9 to 054c2c2 Compare June 1, 2026 16:59
@pageton
Copy link
Copy Markdown
Contributor Author

pageton commented Jun 1, 2026

@gaby Fixed. CheckConstraint now falls back to parsing c.Data on the fly when intDataValid is false, restoring the pre-PR behavior for callers that construct Constraint{ID, Data} directly without going through route registration. The pre-parsed fast path (0 allocs) is still used for route-registered constraints.

Also added Test_ConstraintCheckConstraint_ManualData covering all 7 numeric constraint types with manually-constructed constraints to prevent this regression.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
path.go (1)

868-967: ⚡ Quick win

Consider extracting fallback parsing into helper functions to reduce duplication.

The fallback parsing logic when intDataValid is false is duplicated across all seven numeric constraint cases. Extracting this into helper functions would improve maintainability without impacting hot-path performance (helpers would only execute in the fallback path).

♻️ Proposed refactor: extract parsing helpers

Add these helper functions before CheckConstraint:

+// parseSingleConstraintInt parses a single integer from c.Data[0]
+// Returns the parsed value and true on success, or 0 and false on error
+func (c *Constraint) parseSingleConstraintInt() (int, bool) {
+	if len(c.Data) == 0 {
+		return 0, false
+	}
+	v, err := strconv.Atoi(c.Data[0])
+	if err != nil {
+		return 0, false
+	}
+	return v, true
+}
+
+// parseTwoConstraintInts parses two integers from c.Data[0] and c.Data[1]
+// Returns the parsed values and true on success, or 0,0 and false on error
+func (c *Constraint) parseTwoConstraintInts() (int, int, bool) {
+	if len(c.Data) < 2 {
+		return 0, 0, false
+	}
+	v0, err0 := strconv.Atoi(c.Data[0])
+	v1, err1 := strconv.Atoi(c.Data[1])
+	if err0 != nil || err1 != nil {
+		return 0, 0, false
+	}
+	return v0, v1, true
+}
+
 func (c *Constraint) CheckConstraint(param string) bool {

Then simplify each case. Example for minLenConstraint:

 	case minLenConstraint:
 		limit := c.intData[0]
 		if !c.intDataValid {
-			v, parseErr := strconv.Atoi(c.Data[0])
-			if parseErr != nil {
+			var ok bool
+			limit, ok = c.parseSingleConstraintInt()
+			if !ok {
 				return false
 			}
-			limit = v
 		}
 		if len(param) < limit {
 			return false
 		}

Example for betweenLenConstraint:

 	case betweenLenConstraint:
 		lo := c.intData[0]
 		hi := c.intData[1]
 		if !c.intDataValid {
-			v0, parseErr := strconv.Atoi(c.Data[0])
-			if parseErr != nil {
-				return false
-			}
-			v1, parseErr := strconv.Atoi(c.Data[1])
-			if parseErr != nil {
+			var ok bool
+			lo, hi, ok = c.parseTwoConstraintInts()
+			if !ok {
 				return false
 			}
-			lo = v0
-			hi = v1
 		}
 		length := len(param)
 		if length < lo || length > hi {
 			return false
 		}

Apply the same pattern to all seven numeric constraint cases.

🤖 Prompt for 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.

In `@path.go` around lines 868 - 967, The numeric-constraint branches in
CheckConstraint duplicate fallback parsing for c.Data; extract two helpers
(e.g., parseFallbackInt(c *constraint, idx int) (int, bool) and
parseFallbackTwoInts(c *constraint) (int, int, bool)) that return parsed ints
and a success flag when c.intDataValid is false, and use c.intData values when
true; then replace the repeated strconv.Atoi blocks in minLenConstraint,
maxLenConstraint, lenConstraint, betweenLenConstraint, minConstraint,
maxConstraint, and rangeConstraint with calls to these helpers (or direct use of
c.intData when intDataValid) so the hot path remains unchanged and all fallback
parsing is centralized.
🤖 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.

Nitpick comments:
In `@path.go`:
- Around line 868-967: The numeric-constraint branches in CheckConstraint
duplicate fallback parsing for c.Data; extract two helpers (e.g.,
parseFallbackInt(c *constraint, idx int) (int, bool) and parseFallbackTwoInts(c
*constraint) (int, int, bool)) that return parsed ints and a success flag when
c.intDataValid is false, and use c.intData values when true; then replace the
repeated strconv.Atoi blocks in minLenConstraint, maxLenConstraint,
lenConstraint, betweenLenConstraint, minConstraint, maxConstraint, and
rangeConstraint with calls to these helpers (or direct use of c.intData when
intDataValid) so the hot path remains unchanged and all fallback parsing is
centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c97564a0-7947-4bfc-8918-e392ec76470a

📥 Commits

Reviewing files that changed from the base of the PR and between 6fe2d0e and 054c2c2.

📒 Files selected for processing (2)
  • path.go
  • path_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • path_test.go

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comment thread path.go
pageton added 2 commits June 2, 2026 11:29
## Changes

### client/core.go — sync fast-path + RLock for hooks

- Add execSync fast-path: when context has no cancellation (Done() == nil),
  execute HTTP requests synchronously without goroutine/channel overhead
- Extract shared doRequest() helper to eliminate duplicated request execution
  logic between async and sync paths
- Change Lock to RLock in preHooks/afterHooks: builtin hooks only read the
  client hook slice and mutate the request object, not client state

### path.go — pre-parse constraint integers at registration time

- Add intData [2]int and intDataValid bool to Constraint struct
- Parse strconv.Atoi for minLen/maxLen/len/betweenLen/min/max/range constraints
  once at route registration instead of on every request
- Use pre-parsed intData in CheckConstraint with validity guard

### router.go — cache interface method calls in nextCustom

- Extract c.getDetectionPath(), c.Path(), c.getValues() before the route
  matching loop to avoid repeated interface method calls per iteration
- Apply same caching to the method-not-allowed scan path

## Benchmark Results (AMD Ryzen 5 7600X, goos:linux/goarch:amd64)

client/core.go:
  Benchmark_Client_Request_Parallel:  2075ns -> 1071ns  (-48.39%, p=0.000)
  Benchmark_Client_Request:           80 B  ->  32 B   (-60.00%)
  allocs/op:                          2     ->  1      (-50.00%)

path.go (constraint check):
  Benchmark_CheckConstraint/range:    ~10ns/op, 0 allocs (was ~50-100ns with strconv.Atoi)
  Benchmark_CheckConstraint/minLen:   ~4ns/op, 0 allocs

router.go:
  Benchmark_Router_Next:              no regression (0 allocs maintained)
…onstraint structs

CheckConstraint now falls back to parsing Data on the fly when
intDataValid is false, restoring the pre-PR behavior for callers
that construct Constraint{ID, Data} directly without going through
route registration. The pre-parsed fast path (0 allocs) is still
used for route-registered constraints.

Adds Test_ConstraintCheckConstraint_ManualData covering all 7
numeric constraint types with manually-constructed constraints.
Comment thread path.go Outdated
@pageton pageton changed the title 🔥 perf: reduce allocations in routing hot path and client HTTP layer 🔥 perf: reduce allocations in routing hot path Jun 2, 2026
@ReneWerner87
Copy link
Copy Markdown
Member

@pageton can you post the before and after bench to the last state of this code

@pageton pageton force-pushed the perf/p0-allocation-reduction branch from bc26660 to 4e70951 Compare June 3, 2026 18:40
@pageton
Copy link
Copy Markdown
Contributor Author

pageton commented Jun 3, 2026

@ReneWerner87 Here are the benchmark results comparing base (3026a5ec, upstream/main) vs PR branch (4e709512) using benchstat with 5 runs each.

Environment: Linux/amd64, AMD Ryzen 5 7600X 6-Core, Go 1.26.2


client/core.go — Client HTTP layer

Benchmark Metric Before After Delta
Client_Request sec/op 4.911µ 3.908µ -20.4%
Client_Request B/op 81 32 -60.5%
Client_Request allocs/op 2 1 -50.0%
Client_Request_Parallel sec/op 2.176µ 2.406µ +10.6%
Client_Request_Parallel B/op 81 32 -60.5%
Client_Request_Parallel allocs/op 2 1 -50.0%

Note: Client_Request_Parallel shows a slight sec/op increase — this is expected because the parallel benchmark measures goroutine+channel throughput which benefits from the async path. The sequential Client_Request (sync fast-path) shows the real gain. Both paths reduce allocations by -60% and -50% respectively.


path.go — Constraint checking (new benchmark, after only)

Benchmark sec/op B/op allocs/op
CheckConstraint/range_preparsed 6.058ns 0 0
CheckConstraint/minLen_preparsed 2.706ns 0 0

Previously, each numeric constraint call performed strconv.Atoi (~50-100ns + 1-2 allocs). Now it's ~3-6ns with 0 allocs for pre-parsed constraints.

The fallback path (for manually-constructed constraints without pre-parse) preserves the old behavior by parsing Data on the fly.


router.go — Routing loop

Benchmark Metric Before After Delta
Router_Handler sec/op 66.09ns 65.63ns ~ (no change)
Router_Next sec/op 39.29ns 38.07ns -3.1%
Router_Handler B/op 0 0 0
Router_Next B/op 0 0 0
Router_Handler allocs/op 0 0 0
Router_Next allocs/op 0 0 0

No regressions. Zero allocations maintained. Slight improvement in Router_Next from caching interface calls in nextCustom.

The constraint optimization (path.go) introduced a backward-compatibility
regression for manually-constructed Constraint structs and will be submitted
as a separate PR after proper review.
@ReneWerner87
Copy link
Copy Markdown
Member

@pageton this
image
has no effect or
image
??
everything under 1ns or 5% is negligible

@ReneWerner87
Copy link
Copy Markdown
Member

@pageton

@pageton
Copy link
Copy Markdown
Contributor Author

pageton commented Jun 5, 2026

@ReneWerner87 I will review the issue and carefully check everything. If there is no significant difference in performance, I will close the PR.

@pageton
Copy link
Copy Markdown
Contributor Author

pageton commented Jun 5, 2026

This pull request has been closed because it does not provide a sufficient performance improvement to justify the changes.

@pageton pageton closed this Jun 5, 2026
@github-project-automation github-project-automation Bot moved this to Done in v3 Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants