elastic / elastic/beats

[performance-profiler] Reduce allocations in Thrift formatStruct hot path

Open Beginner friendly
#52,296 1 comment 0 reactions 0 assignees View on GitHub
needs_team
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.