Skip to content

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

Description

@github-actions

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:

--- 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/op96 B/op)
  • 50% fewer allocations (2 allocs/op1 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? | From workflow: Performance Profiler

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

  • expires on Aug 21, 2026, 2:38 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs_teamIndicates that the issue/PR needs a Team:* label

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions