[9.4](backport #50020) perf: optimize FileMetaReader hot path and EncoderReader map sizing#50257
Conversation
…50020) FileMetaReader.Next() is called for every line harvested by filebeat. Replace DeepUpdate (recursive map merge) and dotted-key Put ("log.file.device_id") with direct map assignment. Lazy-cache the per-file metadata (path, device_id, inode, fingerprint) on first call since these values are constant for the file's lifetime. The cached interface{} values are copied via maps.Copy without re-boxing, which is where the alloc savings come from. The cache is built into a local variable and only assigned on success, so a failed setFileSystemMetadata retries on the next call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * perf: pre-size Fields map in EncoderReader to avoid grow on first write EncoderReader.Next() creates a Fields map for every line. Changing from mapstr.M{} (capacity 0) to make(mapstr.M, 1) avoids a runtime map grow when the first downstream consumer writes into it. Benefits all inputs using EncoderReader: filestream, legacy log, and S3. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add changelog fragment and resolve lint issues Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove changelog fragment, using skip-changelog label Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: use require.IsType and consolidate benchmark into metafields_test.go Replace bare type assertion + require.True(ok) patterns with require.IsType in metafields_other_test.go and metafields_windows_test.go. Move benchmark functions from metafields_bench_test.go into metafields_test.go and delete the separate bench file, following the one-test-file-per-processor principle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use comma-ok type assertions to satisfy errcheck linter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * perf: exact cachedMeta pre-sizing via platformFileFields constant Add a platformFileFields constant to each platform file (2 on non-Windows, 3 on Windows) so metafields.go can compute the exact map capacity without over-allocating. Add TestCachedMetaSizing on both platforms to verify the pre-allocation matches the actual entry count. Also document that the direct log key assignment is intentional, and fix TestMetaFieldsOwnerAndGroup to use the platform-correct group name (wheel on macOS, root on Linux). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix testifylint: use require.Len instead of len() comparison The linter requires require.Len(t, x, n) instead of require.Equal(t, n, len(x)). Assisted-By: Cursor Made-with: Cursor * fix testifylint: use require.Len in windows test file Same fix as previous commit, but for the Windows-specific test. Assisted-By: Cursor Made-with: Cursor --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> (cherry picked from commit 5f5774b)
🤖 GitHub commentsJust comment with:
|
|
Pinging @elastic/elastic-agent-data-plane (Team:Elastic-Agent-Data-Plane) |
This comment has been minimized.
This comment has been minimized.
|
/test |
TL;DR
Remediation
Investigation detailsRoot Cause
This is a code bug (type mismatch/invalid symbol), not infrastructure. Evidence
Verification
Follow-upIf there are other backported test hunks around pipeline client race fixes, quickly grep for additional What is this? | From workflow: PR Buildkite Detective Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not. |
~20% faster filestream reader pipeline, 29% fewer allocations per file, benefits all inputs using EncoderReader.
BenchmarkFilestreamexercises the full filestream reader chain: file I/O → line buffering → UTF-8 encoding → newline stripping → file metadata enrichment → parsers → line limiting →ToEvent()→ cursor tracking → publish to pipeline. This measures the complete input-side cost of reading and enriching log lines (no output serialization or network I/O).Commit 1: FileMetaReader — avoid DeepUpdate, cache metadata
Replace
DeepUpdate(recursive map merge) and dotted-keyPutwith direct map assignment. Lazy-cache per-file metadata (path, device_id, inode, fingerprint) on firstNext()call since these values are constant for a file's lifetime. The cachedinterface{}values are copied viamaps.Copywithout re-boxing, which is where the alloc savings come from.Commit 2: EncoderReader — pre-size Fields map
Change
mapstr.M{}(capacity 0) tomake(mapstr.M, 1)so the first downstream write doesn't trigger a map grow. Benefits all inputs usingEncoderReader: filestream, legacy log, and S3.Safety analysis
FileMetaReaderruns in a single harvester goroutine — no concurrent access tocachedMeta.fileMapviamake+maps.Copy. Downstream code (LimitReader, parsers,ToEvent, publishers) can freely mutate the per-event map without corrupting the cache.interface{}wrappers avoids re-boxing.fiis captured at file-open time;GetOSState()returns a stored struct.pathandfingerprintare constructor args.cachedMetais built into a local variable and only assigned on success. A failedsetFileSystemMetadataleavescachedMetanil so the next call retries — identical behavior to the original per-call code.log.file.device_idis identical — only the internal construction path changed from dotted-key traversal to direct map assignment.make(mapstr.M, 1)is a drop-in replacement formapstr.M{}— both produce an empty, non-nil map. The only difference is the capacity hint.len(),== nil, and all standard operations behave identically.Raw benchmark data (benchstat, 6 runs each, Apple M2 Pro)
BenchmarkFilestream — full pipeline, 10k lines
BenchmarkFileMetaReaderNext — isolated FileMetaReader
BenchmarkEncoderReader — commit 2 improvement (benefits all inputs)
Test plan
TestMetaFields/TestMetaFieldsOwnerAndGrouppass (updated for new map structure)BenchmarkFileMetaReaderNextadded for regression detectionGOOS=windows GOARCH=amd64 go build)🤖 Generated with Claude Code
This is an automatic backport of pull request #50020 done by Mergify.