-
-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
Description
The response generation uses many small fmt.Fprintf calls which cause repeated allocations and higher GC pressure. Using strings.Builder would be more efficient.
Location
http-echo.go:106-248 (all print methods)
func (h helloWorldhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
// Multiple fmt.Fprintf calls cause many allocations
h.printRequestSummary(w, info)
h.printURLInfo(w, info)
h.printHeaders(w, info)
h.printRequestBody(w, info)
h.printFormData(w, info)
h.printServerInfo(w, info, startTime)
// ...
}Impact
- Severity: LOW-MEDIUM
- Higher GC pressure under concurrent load
- Slower response times with many simultaneous requests
- Increased memory usage
- Not critical but noticeable at scale
Performance Analysis
Each fmt.Fprintf(w, ...) call:
- Formats the string (allocation)
- Writes to
http.ResponseWriter(buffer allocation) - Repeated many times per request (50+ calls)
Under load (1000 req/s), this creates significant allocation churn.
Recommended Optimization
Use strings.Builder to assemble the entire response, then write once:
func (h helloWorldhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
info := h.collectRequestInfo(r, startTime)
// Pre-allocate builder with estimated response size
var sb strings.Builder
sb.Grow(2048) // Adjust based on profiling
h.printRequestSummary(&sb, info)
h.printURLInfo(&sb, info)
h.printHeaders(&sb, info)
h.printRequestBody(&sb, info)
h.printFormData(&sb, info)
h.printServerInfo(&sb, info, startTime)
fmt.Fprintf(&sb, "\n=== REQUEST COMPLETED ===\n")
fmt.Fprintf(&sb, "Processing Time: %v\n", time.Since(startTime))
// Single write to response
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
if _, err := w.Write([]byte(sb.String())); err != nil {
log.Printf("Write error: %v", err)
}
}Update all print methods to accept io.Writer instead of http.ResponseWriter:
func (h helloWorldhandler) printRequestSummary(w io.Writer, info requestInfo) {
fmt.Fprintf(w, "=== REQUEST SUMMARY ===\n")
// ... rest of method unchanged
}Benefits
- Fewer allocations (1 large allocation vs many small ones)
- Reduced GC pressure
- Better performance under load
- Lower memory usage
- Easier to add Content-Length header (response size known upfront)
Benchmarking
Before optimization:
go test -bench=. -benchmem
# Expected: ~50 allocs/op, ~8KB allocatedAfter optimization:
go test -bench=. -benchmem
# Expected: ~5-10 allocs/op, ~2-4KB allocatedOptional: Add Content-Length
With the full response in memory, we can set Content-Length:
response := sb.String()
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
w.Write([]byte(response))This helps HTTP clients know the response size upfront.
Testing
- Load test before/after with
heyorwrk - Profile with
pprofto measure allocation reduction - Verify response content is identical
# Load test
hey -n 10000 -c 100 http://localhost:8080/
# Memory profiling
go tool pprof -alloc_space http://localhost:8080/debug/pprof/heapPriority
Medium - Performance optimization worth doing if touching the code anyway, especially for high-traffic scenarios.
Trade-offs
- Pro: Better performance, less GC pressure
- Con: Slightly more complex (needs io.Writer abstraction)
- Con: Response fully buffered in memory (negligible for this use case)
References
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels