elastic / elastic/beats

[performance-profiler] Cache timezone offset strings in dtfmt LocalTime hot path

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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.