[performance-profiler] Reduce allocations in Thrift formatStruct hot path
- Dominant language
- Go
- Stars
- 12.7k
- Forks
- 5k
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 364
Description
## Hot Path
`(*thriftPlugin).formatStruct` in `packetbeat/protos/thrift/thrift.go:674-701` is on the request parsing path used by `BenchmarkThrift_ParseSkipReply` (`packetbeat/protos/thrift/thrift_test.go:884-924`).
The pre-change implementation built `[]string` entries and then called `strings.Join`, creating avoidable intermediate strings and slice growth in a high-frequency path.
## Profiling Data
**Before:**
```text
go test -run '^$' -bench '^BenchmarkThrift_ParseSkipReply$' -benchmem -count=5 ./packetbeat/protos/thrift
goos: linux
goarch: amd64
pkg: github.com/elastic/beats/v7/packetbeat/protos/thrift
cpu: AMD EPYC 9V74 80-Core Processor
BenchmarkThrift_ParseSkipReply-4 452058 2692 ns/op 2487 B/op 36 allocs/op
BenchmarkThrift_ParseSkipReply-4 412143 2598 ns/op 2488 B/op 36 allocs/op
BenchmarkThrift_ParseSkipReply-4 478453 2652 ns/op 2487 B/op 36 allocs/op
BenchmarkThrift_ParseSkipReply-4 401570 2570 ns/op 2488 B/op 36 allocs/op
BenchmarkThrift_ParseSkipReply-4 504164 2675 ns/op 2486 B/op 36 allocs/op
```
## Proposed Change
Replace `[]string` accumulation + `strings.Join` with a single `strings.Builder` path that writes separators and field content directly, and use stack-backed numeric formatting (`strconv.AppendInt`) for field IDs.
```diff
--- a/packetbeat/protos/thrift/thrift.go
+++ b/packetbeat/protos/thrift/thrift.go
@@ -674,19 +674,30 @@
- toJoin := []string{}
+ var b strings.Builder
+ b.WriteByte('(')
for i, field := range fields {
if i == thrift.collectionMaxSize {
- toJoin = append(toJoin, "...")
+ if i > 0 {
+ b.WriteString(", ")
+ }
+ b.WriteString("...")
break
}
+ if i > 0 {
+ b.WriteString(", ")
+ }
if resolveNames && int(field.id) < len(fieldnames) && fieldnames[field.id] != nil {
- toJoin = append(toJoin, *fieldnames[field.id]+": "+field.value)
+ b.WriteString(*fieldnames[field.id])
} else {
- toJoin = append(toJoin, strconv.Itoa(int(field.id))+": "+field.value)
+ var idBuf [20]byte
+ b.Write(strconv.AppendInt(idBuf[:0], int64(field.id), 10))
}
+ b.WriteString(": ")
+ b.WriteString(field.value)
}
- return "(" + strings.Join(toJoin, ", ") + ")"
+ b.WriteByte(')')
+ return b.String()
}
```
## Results
**After:**
```text
go test -run '^$' -bench '^BenchmarkThrift_ParseSkipReply$' -benchmem -count=5 ./packetbeat/protos/thrift
goos: linux
goarch: amd64
pkg: github.com/elastic/beats/v7/packetbeat/protos/thrift
cpu: AMD EPYC 9V74 80-Core Processor
BenchmarkThrift_ParseSkipReply-4 482084 2500 ns/op 2431 B/op 32 allocs/op
BenchmarkThrift_ParseSkipReply-4 428326 2496 ns/op 2432 B/op 32 allocs/op
BenchmarkThrift_ParseSkipReply-4 521224 2386 ns/op 2430 B/op 32 allocs/op
BenchmarkThrift_ParseSkipReply-4 520482 2400 ns/op 2430 B/op 32 allocs/op
BenchmarkThrift_ParseSkipReply-4 503142 2403 ns/op 2430 B/op 32 allocs/op
```
**Improvement:**
- Average time: `2637.4 ns/op -> 2437.0 ns/op` (**7.6% faster**)
- Allocations: `36 -> 32 allocs/op` (**11.1% fewer allocs/op**)
- Bytes: `2487.2 -> 2430.6 B/op` (**2.3% lower B/op**)
## Verification
- `go test ./packetbeat/protos/thrift -run '^TestThrift_'` passes after the change.
- Parsing behavior remains unchanged on existing Thrift request/reply tests.
## Evidence
- Benchmark command (same for before/after):
- `go test -run '^$' -bench '^BenchmarkThrift_ParseSkipReply$' -benchmem -count=5 ./packetbeat/protos/thrift`
- Changed code location:
- `packetbeat/protos/thrift/thrift.go:674-701`
- Benchmark definition:
- `packetbeat/protos/thrift/thrift_test.go:884-924`
---
[What is this?](https://ela.st/github-ai-tools) | [From workflow: Performance Profiler](https://github.com/elastic/beats/actions/runs/30370085040)
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
> - [x] expires on Aug 4, 2026, 3:00 PM UTC
Contributor guide
Research direction
Start in packetbeat/protos/thrift/thrift.go:674-701 and compare formatStruct with the proposed builder-based approach. Run the focused BenchmarkThrift_ParseSkipReply benchmark and TestThrift tests in packetbeat/protos/thrift/thrift_test.go; done means parsing behavior remains unchanged while allocations and benchmark time improve.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100