[performance-profiler] Cache timezone offset strings in dtfmt LocalTime hot path
- Dominant language
- Go
- Stars
- 12.7k
- Forks
- 5k
- Avg merge
- 2d 15m
- Merged PRs (30d)
- 385
Description
## Hot Path
`tzOffsetString` in `libbeat/common/dtfmt/fields.go:147-170` is on the local-time JSON encoding path (`libbeat/outputs/codec/json/json_bench_test.go:46-58`). The current implementation allocates on every call by building `[]byte` and converting it to `string`.
## Profiling Data
**Before:**
```
$ go test -run '^$' -bench '^BenchmarkLocalTime$' -benchmem -count=8 ./libbeat/outputs/codec/json
BenchmarkLocalTime-4 1274766 954.9 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1266380 951.5 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1258628 949.9 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1272968 948.1 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1234520 950.9 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1254757 953.1 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1234729 942.6 ns/op 104 B/op 2 allocs/op
BenchmarkLocalTime-4 1256445 954.2 ns/op 104 B/op 2 allocs/op
```
## Proposed Change
Precompute `+HH:MM` / `-HH:MM` timezone strings once in a static lookup table and reuse them at runtime:
```diff
--- a/libbeat/common/dtfmt/fields.go
+++ b/libbeat/common/dtfmt/fields.go
@@
+const (
+ tzOffsetMinMinutes = -24 * 60
+ tzOffsetMaxMinutes = 24 * 60
+)
+
+var tzOffsetStrings = buildTZOffsetStrings()
+
func tzOffsetString(ctx *ctx) (string, error) {
tzOffsetMinutes := ctx.tzOffset / 60
+ if tzOffsetMinutes >= tzOffsetMinMinutes && tzOffsetMinutes <= tzOffsetMaxMinutes {
+ return tzOffsetStrings[tzOffsetMinutes-tzOffsetMinMinutes], nil
+ }
+ return formatTZOffset(tzOffsetMinutes), nil
}
```
`formatTZOffset` retains existing formatting semantics for fallback/out-of-range values.
## Results
**After:**
```
$ go test -run '^$' -bench '^BenchmarkLocalTime$' -benchmem -count=8 ./libbeat/outputs/codec/json
BenchmarkLocalTime-4 1342268 917.2 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1324545 914.9 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1337199 907.8 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1337120 905.7 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1335277 903.6 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1312209 898.6 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1342334 913.2 ns/op 96 B/op 1 allocs/op
BenchmarkLocalTime-4 1333272 916.7 ns/op 96 B/op 1 allocs/op
```
**Improvement:**
- ~4.3% faster (`~950.6 ns/op` → `~909.7 ns/op`)
- 7.7% lower bytes allocated (`104 B/op` → `96 B/op`)
- 50% fewer allocations (`2 allocs/op` → `1 alloc/op`)
## Verification
- `go test ./libbeat/common/dtfmt`
- `go test ./libbeat/outputs/codec/json`
- Added coverage for offset formatting edge cases in `libbeat/common/dtfmt/dtfmt_test.go:177-197`
## Evidence
- Commands run:
- `go test -run '^$' -bench '^BenchmarkLocalTime$' -benchmem -count=8 ./libbeat/outputs/codec/json` (before/after, same command)
- `go test ./libbeat/common/dtfmt`
- `go test ./libbeat/outputs/codec/json`
- Code references:
- Current hot path: `libbeat/common/dtfmt/fields.go:147-170`
- Benchmark: `libbeat/outputs/codec/json/json_bench_test.go:46-58`
- Behavior tests: `libbeat/common/dtfmt/dtfmt_test.go:177-197`
Duplicate check: `/tmp/previous-findings.json` has no open issue for `dtfmt.tzOffsetString` / `BenchmarkLocalTime` (closest closed item is about `add_locale`, not this path).
---
[What is this?](https://ela.st/github-ai-tools) | [From workflow: Performance Profiler](https://github.com/elastic/beats/actions/runs/31809031202)
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
> - [x] expires on Aug 21, 2026, 2:38 PM UTC
Contributor guide
Research direction
Start with tzOffsetString in libbeat/common/dtfmt/fields.go:147-170, then read the LocalTime benchmark in libbeat/outputs/codec/json/json_bench_test.go:46-58 and edge-case tests in libbeat/common/dtfmt/dtfmt_test.go:177-197. Run the listed dtfmt and JSON package tests and the BenchmarkLocalTime command; done means formatting behavior remains covered while the hot path reuses cached in-range offsets and improves the reported allocations.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100