Delete TODO-netflow-memory-footprint.md (#22252)
Costa Tsaousis committed
Apr 23, 2026 at 02:01 UTC
7b860459dfa230b919f1a12e9b309e292914d47d
1 file changed
-1019
TODO-netflow-memory-footprint.md
deleted
-1019
@@ -1,1019 +0,0 @@
1
-## TL;DR
2
-
3
-- Purpose: reduce `netflow.plugin` memory footprint drastically under sustained ingest, with a target of at least 30x reduction versus the currently observed catastrophic baseline on office traffic.
4
-- Costa reported: the running plugin uses about 2.4 GB RAM for roughly 20 flows/s. That is operationally unacceptable and likely scales into failure at higher flow rates.
5
-- This task includes: building a reproducible stress environment, generating/replaying flow traffic, measuring memory with evidence, optimizing the code, exposing Netdata charts for plugin memory accounting/breakdown, and re-measuring the improvement.
6
-- Follow-up scope added after install: investigate the remaining 4 failing `cargo test -p netflow-plugin` tests, find their real root cause, and fix them if they are regressions or latent bugs in this worktree.
7
-- Follow-up scope added after live validation on `2026-04-11`: the running plugin now shows about `700 MB` RSS in the local environment, and the current Netdata memory charts leave about `98%` of that RSS in `unaccounted`. This phase is to identify where that live heap goes, add subsystem-level attribution for the dominant owners, and produce a concrete runtime breakdown instead of an opaque `unaccounted` bucket.
8
-- Follow-up scope added after commit `d6e4ff75c0` on `2026-04-11`: rerun the full relevant test suites on the committed memory/rebuild changes, fix any regressions immediately, and then continue with the unresolved live anonymous `mmap_in_use` owner until it is concretely attributed and reduced.
9
-- Follow-up scope added after the retention-policy install on `2026-04-12`: review every PR change that touches shared components outside `netflow-plugin`, verify whether each one is truly necessary for the netflow work, identify any shared-layer risk or overreach, and then define a credible performance-testing plan for NetFlow/sFlow/IPFIX ingestion.
10
-
11
-## Analysis
12
-
13
-### Verified code paths
14
-
15
-- The plugin process instantiates all major in-memory subsystems at startup in [`src/crates/netdata-netflow/netflow-plugin/src/main.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/main.rs):
16
- - `IngestMetrics`
17
- - `OpenTierState`
18
- - `TierFlowIndexStore`
19
- - `FacetRuntime`
20
- - `FlowQueryService`
21
-- The plugin now exposes internal memory charts in [`src/crates/netdata-netflow/netflow-plugin/src/charts/metrics.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/charts/metrics.rs), [`src/crates/netdata-netflow/netflow-plugin/src/charts/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/charts/runtime.rs), and [`src/crates/netdata-netflow/netflow-plugin/src/charts/snapshot.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/charts/snapshot.rs):
22
- - `netflow.memory_resident_bytes`
23
- - `netflow.memory_accounted_bytes`
24
- - `netflow.memory_tier_index_bytes`
25
-- Verified charting gap on `2026-04-11`:
26
- - the runtime sampler in [`src/crates/netdata-netflow/netflow-plugin/src/charts/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/charts/runtime.rs) only reads `/proc/self/status` and `/proc/self/smaps_rollup`
27
- - this means it can capture totals like `rss`, `rss_anon`, `rss_file`, and `anon_huge_pages`, but it cannot classify which mappings own those bytes
28
- - as a result, the live `memory_accounted_bytes` chart still reports about `97.6%` of RSS as `unaccounted`, even though `/proc/<pid>/maps` and `/proc/<pid>/smaps` clearly show bounded raw/1m journal mmaps, heap, and large anonymous mappings as separate owners
29
-- Verified live state after installing the mmap-backed GeoIP build on `2026-04-11` late evening:
30
- - the currently running plugin (`PID 1874329`) is now:
31
- - `VmRSS = 86,204 kB`
32
- - `RssAnon = 27,672 kB`
33
- - `RssFile = 58,532 kB`
34
- - `Threads = 8`
35
- - `AnonHugePages = 0 kB`
36
- - `/proc/1874329/maps` now shows the GeoIP databases as shared read-only file mappings instead of copied anonymous memory:
37
- - `/usr/share/netdata/topology-ip-intel/topology-ip-geo.mmdb`
38
- - `/var/cache/netdata/topology-ip-intel/topology-ip-asn.mmdb`
39
- - `/proc/1874329/smaps` shows current resident usage for those mappings at:
40
- - `topology-ip-geo.mmdb`: `29,116 kB RSS`
41
- - `topology-ip-asn.mmdb`: `5,960 kB RSS`
42
- - direct implication:
43
- - the old large anonymous `~138.5 MB` owner is gone in the live process
44
- - the GeoIP optimization is now proven live, but as a file-backed/shared mapping reduction, not as an anonymous-memory reduction inside the old binary
45
- - current top resident classes from live `pmap` / `smaps` are now:
46
- - main anonymous heap/arena mapping: about `26,024 kB RSS`
47
- - GeoIP geo MMDB: about `29,116 kB RSS`
48
- - GeoIP ASN MMDB: about `5,960 kB RSS`
49
- - the rest split across smaller journal and file-backed mappings
50
-- Verified runtime/chart startup attribution on `2026-04-11`:
51
- - the startup profiling harness now exercises the real plugin-runtime path on in-memory streams:
52
- - constructs `PluginRuntime`
53
- - registers the `flows` handler
54
- - registers charts
55
- - starts runtime/chart emission long enough to initialize and emit
56
- - release measurement on the live dataset snapshot:
57
- - `plugin_runtime_configured delta_rss = 37,027,840`
58
- - `plugin_runtime_started delta_rss = 37,359,616`
59
- - incremental runtime+chart cost over the already settled rebuild state: about `331,776 bytes`
60
- - implication:
61
- - the plugin framework path is not the missing large memory owner
62
- - the remaining steady-state memory is explained by the already loaded runtime state plus file-backed MMDB/journal residency, not by a hidden `PluginRuntime` explosion
63
-- Verified dominant live anonymous-memory owner on `2026-04-11`:
64
- - the running plugin shows one large anonymous mapping at about `138.5 MB` RSS, and the allocator chart reports `mmap_in_use ≈ 143.2 MB`
65
- - the stock runtime config auto-detects GeoIP MMDB files even when paths are omitted in YAML:
66
- - [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs)
67
- - [`/usr/lib/netdata/conf.d/netflow.yaml`](/usr/lib/netdata/conf.d/netflow.yaml)
68
- - the plugin currently loads GeoIP databases with `Reader<Vec<u8>>`, which copies the full files into anonymous RAM:
69
- - [`src/crates/netdata-netflow/netflow-plugin/src/enrichment/data/geoip/files.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/enrichment/data/geoip/files.rs)
70
- - [`src/crates/netdata-netflow/netflow-plugin/src/enrichment/data/geoip/types.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/enrichment/data/geoip/types.rs)
71
- - the detected database sizes on this workstation are:
72
- - `/var/cache/netdata/topology-ip-intel/topology-ip-asn.mmdb = 10,402,916 bytes`
73
- - `/usr/share/netdata/topology-ip-intel/topology-ip-geo.mmdb = 131,453,438 bytes`
74
- - combined = `141,856,354 bytes`
75
- - this nearly matches the live anonymous `mmap_in_use` / `anon_other` footprint, making GeoIP DB loading the strongest verified remaining owner
76
- - the local `maxminddb 0.25.0` crate already supports `Reader::open_mmap()` behind its `mmap` feature, so this is optimizable without changing user-facing enrichment behavior
77
- - superseded by the later live result above:
78
- - after the mmap-backed build was actually installed and running, the large anonymous owner disappeared and the MMDBs moved to file-backed residency as expected
79
-- Ingestion writes every decoded flow to the raw journal and also updates:
80
- - facet state on each raw write in [`src/crates/netdata-netflow/netflow-plugin/src/ingest/service/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/service/runtime.rs)
81
- - materialized tier accumulators and tier flow indexes in [`src/crates/netdata-netflow/netflow-plugin/src/ingest/service/tiers.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/service/tiers.rs)
82
-- `FacetRuntime` stores global facet vocabularies and active-file contributions in memory in [`src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs).
83
-- Facet storage includes high-cardinality fields such as `SRC_ADDR`, `DST_ADDR`, `EXPORTER_IP`, ports, ASNs, and text labels, via [`src/crates/netdata-netflow/netflow-plugin/src/facet_catalog.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_catalog.rs).
84
-- The facet store keeps unique values in memory using:
85
- - `TextValueStore`
86
- - `DenseBitSet`
87
- - `RoaringTreemap`
88
- - `IpValueStore`
89
- in [`src/crates/netdata-netflow/netflow-plugin/src/facet_runtime/store.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_runtime/store.rs).
90
-- Active facet contributions are stored per active journal file as `BTreeMap<String, BTreeSet<String>>` in [`src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs).
91
-- Materialized tier indexes keep one `FlowIndex` per active hour bucket in [`src/crates/netdata-netflow/netflow-plugin/src/tiering/index/store.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/tiering/index/store.rs).
92
-- Open tier rows snapshot all currently open aggregate rows into vectors in [`src/crates/netdata-netflow/netflow-plugin/src/tiering/model.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/tiering/model.rs) and [`src/crates/netdata-netflow/netflow-plugin/src/ingest/service/tiers.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/service/tiers.rs).
93
-
94
-### Verified existing test assets
95
-
96
-- There are existing NetFlow/IPFIX/sFlow PCAP fixtures under [`src/crates/netdata-netflow/netflow-plugin/testdata/flows`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/testdata/flows).
97
-- There is already end-to-end UDP replay test scaffolding in [`src/crates/netdata-netflow/netflow-plugin/src/main_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/main_tests.rs), including:
98
- - UDP listener reservation
99
- - PCAP payload extraction
100
- - fixture replay over UDP
101
-
102
-### Measured hotspot evidence
103
-
104
-- A focused synthetic facet stress test now exists in [`src/crates/netdata-netflow/netflow-plugin/src/memory_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/memory_tests.rs) and was executed with:
105
- - `cargo test -p netflow-plugin stress_profile_high_cardinality_facet_runtime_memory -- --ignored --nocapture`
106
- - Baseline result before optimization: RSS grew from `9,715,712` to `30,953,472` bytes.
107
- - Baseline delta: `21,237,760` bytes for `50,000` synthetic high-cardinality flows.
108
- - Current result after the facet/runtime storage refactors: RSS grew from `10,420,224` to `18,903,040` bytes.
109
- - Current delta: `8,482,816` bytes for the same `50,000` synthetic high-cardinality flows.
110
- - Verified focused improvement for this hotspot: about `2.50x` lower RSS delta (`21.24 MB -> 8.48 MB`), still far from the requested overall `30x` target.
111
-- The same file contains a focused tier index stress test, executed with:
112
- - `cargo test -p netflow-plugin stress_profile_high_cardinality_tier_index_memory -- --ignored --nocapture`
113
- - Previous measured hotspot before the sparse row/index work: RSS grew from `6,823,936` to `29,032,448` bytes.
114
- - Previous delta: `22,208,512` bytes for `50,000` synthetic high-cardinality flows.
115
- - Current result after sparse/default-aware rollup rows and IPv4-specific IP storage: RSS grew from `6,967,296` to `16,265,216` bytes.
116
- - Current delta: `9,297,920` bytes for `50,000` synthetic high-cardinality flows.
117
- - Internal accounted heap for this subsystem is now `7,481,034` bytes with this measured breakdown:
118
- - row storage: `5,505,308`
119
- - field stores: `1,685,818`
120
- - flow lookup: `286,720`
121
- - schema: `2,668`
122
- - hour-key index metadata: `8`
123
- - scratch field ids: `512`
124
- - Verified focused improvement for this hotspot: about `2.39x` lower RSS delta (`22.21 MB -> 9.30 MB`).
125
-- The end-to-end synthetic ingest stress harness in the same file was executed with:
126
- - `cargo test -p netflow-plugin stress_profile_high_cardinality_netflow_memory -- --ignored --nocapture`
127
- - Current result: RSS peaked at `19,611,648` bytes from a `11,640,832` byte baseline.
128
- - Current peak delta: `7,970,816` bytes on the `5,000` flow synthetic ingest harness.
129
-- A focused decoder source-port churn stress test now exists in [`src/crates/netdata-netflow/netflow-plugin/src/memory_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/memory_tests.rs) and was executed before and after normalizing decoder parser scope away from raw UDP source ports:
130
- - `cargo test -p netflow-plugin stress_profile_decoder_source_port_churn_memory -- --ignored --nocapture`
131
- - Before normalization:
132
- - RSS grew from `7,118,848` to `875,028,480` bytes.
133
- - Delta: `867,909,632` bytes.
134
- - Parser scopes: `v9=20,000`.
135
- - After normalization:
136
- - RSS grew from `7,401,472` to `9,420,800` bytes.
137
- - Delta: `2,019,328` bytes.
138
- - Parser scopes: `v9=1`.
139
- - Verified focused improvement for this hotspot: about `430x` lower RSS delta on the same workload, caused by collapsing parser/template scope for one exporter IP across source-port churn.
140
-- The top facet cardinalities observed from the facet stress test were:
141
- - `DST_ADDR`: `50,000`
142
- - `SRC_ADDR`: `50,000`
143
- - `SRC_PORT`: `50,000`
144
- - `EXPORTER_IP`: `49,805`
145
- - `DST_PORT`: `13,107`
146
- - `DST_AS`: `4,096`
147
- - `DST_AS_NAME`: `4,096`
148
- - `IN_IF`: `4,096`
149
- - `OUT_IF`: `4,096`
150
- - `SRC_AS`: `4,096`
151
- - `SRC_AS_NAME`: `4,096`
152
- - `EXPORTER_NAME`: `256`
153
-
154
-### Live allocator and runtime evidence on 2026-04-11
155
-
156
-- The installed plugin now exports allocator-specific charts in addition to the existing resident/accounted charts:
157
- - `netflow.memory_allocator_bytes`
158
- - `netflow.memory_accounted_bytes`
159
- - `netflow.memory_resident_bytes`
160
-- Before the latest allocator-trim hook, the running plugin reported:
161
- - `rss = 636,342,300`
162
- - `unaccounted = 623,447,500`
163
- - allocator view:
164
- - `heap_arena = 488,673,300`
165
- - `heap_free = 421,649,500`
166
- - `heap_in_use = 67,023,820`
167
- - `mmap_in_use = 165,027,950`
168
- - `releasable = 15,486,688`
169
-- After adding a post-facet-reconcile `malloc_trim(0)` hook in [`src/crates/netdata-netflow/netflow-plugin/src/query/service.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/query/service.rs), the running plugin dropped to about half the live RSS:
170
- - `rss = 309,284,900`
171
- - `unaccounted = 296,349,500`
172
- - allocator view:
173
- - `heap_arena = 478,384,100`
174
- - `heap_free = 411,117,500`
175
- - `heap_in_use = 67,266,620`
176
- - `mmap_in_use = 165,027,950`
177
- - `releasable = 26,359,620`
178
-- Kernel/process evidence for the running post-trim process (`PID 1267128`) shows the remaining memory is still dominated by anonymous mappings:
179
- - `/proc/1267128/smaps_rollup`:
180
- - `Rss: 303,888 kB`
181
- - `Private_Dirty: 288,944 kB`
182
- - `AnonHugePages: 188,416 kB`
183
- - top resident mappings:
184
- - unlabeled anonymous mapping: `128,376 kB RSS`, `124,928 kB AnonHugePages`
185
- - `[heap]`: `85,460 kB RSS`, `26,624 kB AnonHugePages`
186
- - anonymous mapping at `7f0120000000-7f01221e5000`: `33,972 kB RSS`
187
- - anonymous mapping at `7f00f8000000-7f00fc000000`: `20,604 kB RSS`
188
- - anonymous mapping at `7f011c000000-7f0120000000`: `14,096 kB RSS`
189
-- These large anonymous mappings are aligned and sized like glibc secondary arena mappings, not like flow journals.
190
-- The running process has far more Tokio threads than the plugin workload appears to justify:
191
- - `/proc/1267128/status`: `Threads: 69`
192
- - CPU allowance for the process: `Cpus_allowed_list: 0-23`
193
- - `comm` entries show almost all threads are `tokio-runtime-w`
194
- - this is strong evidence that allocator arenas and thread-pool sizing are a major remaining memory driver
195
-- A release-profile startup harness was run against the live journal directory with:
196
- - `env MALLOC_ARENA_MAX=1 cargo test --release -p netflow-plugin stress_profile_live_startup_memory -- --ignored --nocapture`
197
- - `env MALLOC_ARENA_MAX=4 cargo test --release -p netflow-plugin stress_profile_live_startup_memory -- --ignored --nocapture`
198
-- Both release harness runs were effectively identical because the harness uses `#[tokio::test(flavor = "current_thread")]` and does not reproduce the live multithreaded runtime:
199
- - `MALLOC_ARENA_MAX=1`: `49,917,952` byte final RSS delta
200
- - `MALLOC_ARENA_MAX=4`: `49,885,184` byte final RSS delta
201
-- Conclusion from this evidence:
202
- - the on-disk journal/facet restore path explains about `50 MB`
203
- - the remaining `~250 MB` live gap is now most likely runtime-thread/allocator behavior, not just retained flow/journal structures
204
-
205
-### Rebuild-path root cause verified on 2026-04-11
206
-
207
-- The earlier startup harness was incomplete because it stopped at `IngestService::new_with_facet_runtime()` and never ran the production startup rebuild path:
208
- - [`src/crates/netdata-netflow/netflow-plugin/src/ingest/service/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/service/runtime.rs) calls `self.rebuild_materialized_from_raw().await?` before the UDP receive loop starts.
209
-- A new multithreaded release harness was added in [`src/crates/netdata-netflow/netflow-plugin/src/startup_memory_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/startup_memory_tests.rs) to reproduce the real startup path, thread pools, allocator counters, and rebuild step.
210
-- Verified pre-fix rebuild behavior with the multithread harness:
211
- - `env NETFLOW_PROFILE_WORKER_THREADS=24 NETFLOW_PROFILE_MAX_BLOCKING_THREADS=64 cargo test --release -p netflow-plugin stress_profile_live_startup_memory_multithreaded -- --ignored --nocapture`
212
- - rebuild phase before the latest fixes:
213
- - RSS: about `637 MB`
214
- - threads: `116`
215
- - `heap_in_use`: about `56 MB`
216
- - `heap_free`: about `603 MB`
217
- - This proved the catastrophic resident growth was primarily allocator retention created during rebuild, not live in-memory NetFlow state.
218
-- Verified that trimming only after facet reconcile was insufficient:
219
- - rebuild still settled around `272-286 MB` RSS even after the first rebuild trim
220
- - a second trim after a `12s` settle window recovered only about `0-5 MB`
221
-- The remaining long-lived owner was identified as the global Rayon pool used by journal indexing:
222
- - [`src/crates/journal-engine/src/indexing.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-engine/src/indexing.rs) used `into_par_iter()` on Rayon’s global pool inside `batch_compute_file_indexes()`
223
- - multithread harness after rebuild settled at `31` threads with `workers=4`, which matches `7` Tokio threads plus a persistent `24`-thread Rayon global pool
224
-- Fix implemented:
225
- - [`src/crates/netdata-netflow/netflow-plugin/src/ingest/rebuild.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/rebuild.rs)
226
- - trim glibc heap after raw rebuild completes
227
- - keep rebuild-only heavy objects scoped so they are eligible for release before the post-rebuild trim
228
- - [`src/crates/journal-engine/src/indexing.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-engine/src/indexing.rs)
229
- - replace Rayon global-pool indexing with a bounded local Rayon pool capped at `4` threads
230
-- Verified post-fix rebuild behavior:
231
- - `workers=4`, `max_blocking=8`
232
- - rebuild RSS: about `137 MB`
233
- - rebuild settle RSS: about `137 MB`
234
- - threads after settle: `7`
235
- - `workers=24`, `max_blocking=64`
236
- - rebuild RSS: about `152 MB`
237
- - rebuild settle RSS: about `151 MB`
238
- - threads after settle: `27`
239
-- Net effect of the verified rebuild-path fixes on the same local journal directory:
240
- - from about `637 MB` rebuild RSS down to about `152 MB` with the runtime shape closest to the real service on this host
241
- - about `4.2x` lower resident memory for the verified catastrophic startup path
242
- - from `116` threads down to `27` after rebuild settles
243
-
244
-### Verified structural causes
245
-
246
-- `FacetRuntime` used to keep the same logical vocabulary in multiple resident forms in [`src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs):
247
- - before the refactor it kept `archived_fields`, `fields`, and `active_contributions`
248
- - after the refactor it keeps `archived_fields`, `active_fields`, `active_contributions`, and a lightweight published snapshot
249
-- `active_contributions` no longer stores per-file values as `BTreeSet<String>`. It now uses typed `FacetStore` containers, so IPs, ports, and ASNs stay in compact representations instead of always becoming heap strings.
250
-- The full duplicate combined facet store (`fields`) has been removed from runtime memory. The plugin now keeps archived stores, active stores, per-file active contributions, and lightweight published metadata instead.
251
-- The tier index hot path in [`src/crates/netdata-netflow/netflow-plugin/src/tiering/index/store.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/tiering/index/store.rs) uses the custom `FlowIndex` in [`src/crates/netdata-netflow/flow-index/src/lib.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/flow-index/src/lib.rs), which interns field values but still stores a full `u32` field-id tuple per unique rollup flow.
252
-- The tier index now uses sparse/default-aware rollup rows instead of a fully dense `u32` tuple per stored flow, and it uses a smaller IPv4-specific representation for IP field dictionaries in the `FlowIndex` crate.
253
-- The facet IP store now uses bitmap-backed IPv4 sets and explicit IPv6 storage instead of a generic 17-byte packed representation for all IP addresses.
254
-- The local frontend gap notes already document that high-cardinality facets should not be fully enumerated inline and instead need text-input or autocomplete behavior in [`/home/costa/src/dashboard/cloud-frontend/TODO-flows-gaps.md`](/home/costa/src/dashboard/cloud-frontend/TODO-flows-gaps.md).
255
-- The upstream `netflow_parser::scoped_parser::AutoScopedParser` keys parser/template caches by full `SocketAddr`, not exporter identity:
256
- - `ipfix_parsers: HashMap<IpfixSourceKey, NetflowParser>`
257
- - `v9_parsers: HashMap<V9SourceKey, NetflowParser>`
258
- - `legacy_parsers: HashMap<SocketAddr, NetflowParser>`
259
- in [`/home/costa/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/netflow_parser-0.9.0/src/scoped_parser.rs`](/home/costa/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/netflow_parser-0.9.0/src/scoped_parser.rs).
260
-- The plugin also tracks hydrated decoder namespaces per full `SocketAddr` in [`src/crates/netdata-netflow/netflow-plugin/src/decoder/state/runtime/init.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/decoder/state/runtime/init.rs) and [`src/crates/netdata-netflow/netflow-plugin/src/ingest/persistence.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/persistence.rs).
261
-
262
-### Shared-component review on 2026-04-12
263
-
264
-- Reviewed shared crates changed by the PR against `origin/master`:
265
- - `journal-core`
266
- - `journal-engine`
267
- - `journal-log-writer`
268
- - `netdata-plugin/rt`
269
- - `journal-common`
270
- - `jf/journal_file`
271
-- Shared changes that are justified by the netflow work:
272
- - [`src/crates/journal-core/src/file/mmap.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/mmap.rs)
273
- - required to stop append-heavy writer windows from growing from file start toward file tail, which matched the live journal RSS explosion
274
- - [`src/crates/journal-engine/src/indexing.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-engine/src/indexing.rs)
275
- - `without_disk_cache()` is required by [`src/crates/netdata-netflow/netflow-plugin/src/ingest/rebuild.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/rebuild.rs) to avoid rebuild-time cache files and resident disk-cache overhead
276
- - the bounded local Rayon pool is required to eliminate the persistent global indexing pool that previously inflated rebuild threads and allocator arenas
277
- - [`src/crates/journal-engine/src/logs/query.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-engine/src/logs/query.rs)
278
- - the `merge_log_entries()` allocation fix is required because netflow rebuild executes an unlimited `LogQuery::execute()` path in [`src/crates/netdata-netflow/netflow-plugin/src/ingest/rebuild.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/rebuild.rs)
279
- - [`src/crates/journal-core/src/file/reader.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/reader.rs)
280
- - `build_filter()` is directly consumed by netflow direct journal scans in [`src/crates/netdata-netflow/netflow-plugin/src/query/scan/direct.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/query/scan/direct.rs)
281
- - [`src/crates/journal-log-writer/src/log/mod.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/mod.rs), [`src/crates/journal-log-writer/src/log/chain.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/chain.rs), and [`src/crates/journal-common/src/time.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-common/src/time.rs)
282
- - required for event-time journal writes (`EntryTimestamps` with realtime override), active-file visibility, and lifecycle notifications used by the facet runtime
283
- - [`src/crates/netdata-plugin/rt/src/netdata_env.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-plugin/rt/src/netdata_env.rs)
284
- - `stock_data_dir` is required by netflow GeoIP auto-detection in [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs)
285
-- Shared changes that look like scope expansion rather than netflow necessity:
286
- - [`src/crates/netdata-plugin/rt/src/lib.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-plugin/rt/src/lib.rs)
287
- - the `flows:*` GET-args-to-JSON shim is netflow-specific behavior inside the shared runtime
288
- - `ProgressState::snapshot()`, cancellation `499` behavior, and their new tests are not required by the production netflow path; current references are only test-side
289
- - [`src/crates/journal-engine/src/logs/query.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-engine/src/logs/query.rs)
290
- - `with_output_fields()` is not used by netflow production code; repository search found only self-tests as callers
291
- - [`src/crates/journal-core/src/file/file.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/file.rs) and [`src/crates/jf/journal_file/src/file.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/jf/journal_file/src/file.rs)
292
- - the zero-offset iterator hardening is a valid defensive fix, but it is not tied to a reproduced netflow requirement
293
- - the `jf/journal_file` copy is especially outside the netflow scope
294
-- Netflow-local extraction that is acceptable and should stay:
295
- - [`src/crates/netdata-netflow/flow-index/src/lib.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/flow-index/src/lib.rs)
296
- - this is a netflow-owned crate, not a general shared runtime/library change
297
- - it cleanly isolates the compact rollup/grouping index logic and memory accounting used by the plugin
298
-- This creates a verified memory-risk pattern: if the same exporter IP changes UDP source port over time, parser/template caches can grow by source-port churn even when exporter identity is operationally unchanged.
299
-- The plugin entrypoint in [`src/crates/netdata-netflow/netflow-plugin/src/main.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/main.rs) uses `#[tokio::main]`, which means:
300
- - Tokio chooses the multithread runtime defaults automatically
301
- - worker thread count is not explicitly bounded for this plugin
302
- - blocking-thread pool limits are not explicitly bounded for this plugin
303
-- This matters because the live process currently shows `69` threads, mostly `tokio-runtime-w`, on a machine where `nproc` and `getconf _NPROCESSORS_ONLN` both report `24`.
304
-- The current startup harness therefore underestimates live memory by construction, because it uses `current_thread` runtime flavor and never reproduces the production thread-pool shape.
305
-
306
-### Current accounted facet breakdown
307
-
308
-- The focused facet profile now exposes an internal accounted breakdown from `FacetRuntime::estimated_memory_breakdown()`:
309
- - archived facet stores: `972,497` bytes
310
- - active facet stores: `572,521` bytes
311
- - active per-file contributions: `505,107` bytes
312
- - published snapshot metadata: `3,005` bytes
313
- - archived path tracking: `152` bytes
314
-- This shows the duplicate combined store is gone and high-cardinality IP storage is materially smaller, but the total measured reduction is still nowhere near the requested `30x`.
315
-- Re-validated on `2026-04-11` after the latest live-memory fixes:
316
- - `cargo test -p netflow-plugin stress_profile_high_cardinality_facet_runtime_memory -- --ignored --nocapture`
317
- - Measured RSS growth: `8,966,144` bytes
318
- - Current facet-accounted total: `2,053,282` bytes
319
- - Verified undercount factor on this focused harness: about `4.37x`
320
- - This matters because the live process currently reports only about `12 MB` of facet memory in charts, while the startup harness and this focused profile strongly suggest the real facet footprint is materially larger.
321
-- Re-validated on `2026-04-11` with the live-startup multithread harness after disabling rebuild disk cache:
322
- - `cargo test -p netflow-plugin --release stress_profile_live_startup_memory_multithreaded -- --ignored --nocapture`
323
- - `facet_runtime_new` raised RSS by `48,795,648` bytes from baseline
324
- - `query_service_new` added only `856,064` bytes more
325
- - `ingest_service_new` added only `684,032` bytes more
326
- - `rebuild_materialized_from_raw` still settles around `151.8 MB` RSS
327
- - This confirms the remaining live `~315 MB` process is not just startup rebuild; a large steady-state owner remains, and facet memory accounting is one verified blind spot.
328
-- Re-validated on `2026-04-11` after removing the duplicate active store and making `DenseBitSet` lazy:
329
- - `cargo test -p netflow-plugin stress_profile_high_cardinality_facet_runtime_memory -- --ignored --nocapture`
330
- - focused facet RSS delta improved from `8,966,144` bytes to `8,302,592` bytes
331
- - a later compact persisted-text experiment improved this focused harness only marginally again (`8,302,592 -> 8,228,864` bytes), which is too small to justify a schema change by itself
332
- - `NETFLOW_PROFILE_SETTLE_SECS=20 cargo test -p netflow-plugin --release stress_profile_live_startup_memory_multithreaded -- --ignored --nocapture`
333
- - startup `rebuild_settle_trim` improved materially with the validated active-store removal and lazy bitset work: about `145.36 MB -> 124.69 MB`
334
-- Re-validated on `2026-04-11` after removing internal timestamp facets from the user-facing catalog and request allowlists:
335
- - `env MALLOC_ARENA_MAX=1 NETFLOW_PROFILE_SETTLE_SECS=5 NETFLOW_PROFILE_DISABLE_THP=1 cargo test -p netflow-plugin --release stress_profile_live_startup_memory_multithreaded -- --ignored --nocapture`
336
- - measured startup phases on the retained live dataset:
337
- - `facet_runtime_new delta_rss = 10,301,440`
338
- - `query_service_new delta_rss = 11,055,104`
339
- - `ingest_service_new delta_rss = 11,673,600`
340
- - `rebuild_materialized_from_raw delta_rss = 15,089,664`
341
- - `rebuild_settle_trim delta_rss = 15,052,800`
342
- - measured rebuild-time facet breakdown:
343
- - `archived = 3,598,723`
344
- - `published = 13,812`
345
- - `archived_paths = 55,050`
346
- - implication:
347
- - the archived timestamp-field removal is visible not only in deep allocative accounting but also in the startup harness; archived facet state is now a much smaller contributor during rebuild/load
348
-- Verified profiling confounder on `2026-04-11`:
349
- - the installed live plugin at [`/usr/libexec/netdata/plugins.d/netflow-plugin`](/usr/libexec/netdata/plugins.d/netflow-plugin) is still older code and continues writing `/var/cache/netdata/flows/facet-state.bin`
350
- - direct decode of the live `facet-state.bin` under the experimental compact persisted-text schema failed with `UnexpectedEof`
351
- - raw bytes inspection showed the file header still begins with `04 00 00 00`, consistent with the older on-disk schema, not the experimental bumped version
352
- - conclusion: startup profiling against `/var/cache/netdata/flows` is contaminated for persisted-state comparisons until the running service is updated or profiling is moved to an isolated snapshot
353
- - action taken: reverted the unproven compact persisted-text schema change and kept only the validated in-memory facet optimizations
354
-
355
-## Feasibility Assessment
356
-
357
-### Assumptions checked
358
-
359
-1. Assumption: the plugin can be run in a local isolated environment without touching production services.
360
- - Verified: `PluginConfig::new()` supports Netdata-style config discovery from environment variables in [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs) and [`src/crates/netdata-plugin/rt/src/netdata_env.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-plugin/rt/src/netdata_env.rs).
361
-2. Assumption: we can generate realistic ingest load without office routers.
362
- - Verified: existing PCAP fixtures and replay helpers already provide a base path for UDP ingestion tests in [`src/crates/netdata-netflow/netflow-plugin/src/main_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/main_tests.rs).
363
-3. Assumption: we can profile memory locally.
364
- - Verified: `/usr/bin/time`, `perf`, and `valgrind` are available on this machine.
365
-4. Assumption: the current worktree is clean enough to make isolated changes.
366
- - Verified false: `git status --short` now shows tracked changes in the netflow plugin crate plus untracked local helper files (`TODO-netflow-memory-footprint.md`, `build-install-netflow-plugin.sh`).
367
- - This is not a blocker because the modified files are scoped to the current task and the untracked files are intentionally left out of commits.
368
-
369
-### Feasibility verdict
370
-
371
-FEASIBLE AS SPECIFIED
372
-
373
-The codebase already contains the pieces needed to build a reproducible stress/profiling harness and to validate memory reductions locally before reporting results.
374
-
375
-## Decisions
376
-
377
-- Decision made by Costa on `2026-04-12`: clean up unnecessary shared-component changes before proceeding with flow-ingestion performance work.
378
- - Scope to clean first:
379
- - netflow-specific `flows:*` request parsing currently living in shared `rt`
380
- - shared-library additions that are not required by the netflow production path
381
- - duplicated/shared defensive fixes that do not benefit the netflow path and only expand review surface
382
- - Purpose:
383
- - keep the PR reviewable
384
- - keep shared crates generic
385
- - ensure the later performance-testing work is built on the code we actually intend to upstream
386
- - Implemented on `2026-04-12`:
387
- - moved the `flows:*` GET-argument compatibility shim out of shared [`src/crates/netdata-plugin/rt/src/lib.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-plugin/rt/src/lib.rs) and into netflow-local request parsing in [`src/crates/netdata-netflow/netflow-plugin/src/api/flows/handler.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/api/flows/handler.rs)
388
- - removed unused shared `journal-engine` API surface `LogQuery::with_output_fields()` from [`src/crates/journal-engine/src/logs/query.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-engine/src/logs/query.rs)
389
- - removed the duplicated zero-offset iterator hardening and test from the out-of-path [`src/crates/jf/journal_file/src/file.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/jf/journal_file/src/file.rs)
390
- - Verification on `2026-04-12`:
391
- - `cargo test -p rt --manifest-path src/crates/Cargo.toml` passed
392
- - `cargo test -p journal-engine --manifest-path src/crates/Cargo.toml` passed
393
- - `cargo test -p journal-core --manifest-path src/crates/Cargo.toml` passed
394
- - `cargo test -p journal-log-writer --manifest-path src/crates/Cargo.toml` passed
395
- - `cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml` passed with `388 passed, 0 failed, 14 ignored`
396
- - standalone duplicate crate check:
397
- - `cargo test --manifest-path src/crates/jf/journal_file/Cargo.toml writer::tests::test_write_and_read_journal_entries -- --nocapture`
398
- - failed both on the cleanup worktree and on baseline checkpoint commit `973f200dd5`
399
- - panic is in [`src/crates/jf/journal_file/src/writer.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/jf/journal_file/src/writer.rs) at line `637`, not in the reverted iterator code
400
- - implication: this `jf` failure is pre-existing noise for the duplicated crate, not a regression introduced by the cleanup
401
-- Decision made autonomously on `2026-04-12` for the first ingestion-performance pass:
402
- - implement the benchmark as ignored `cargo test` harnesses inside `netflow-plugin`, not as Criterion benches yet
403
- - Evidence:
404
- - the repo already uses ignored profiling tests for startup, memory, and query profiling
405
- - the existing ingest throughput harness was already in [`src/crates/netdata-netflow/netflow-plugin/src/ingest_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest_tests.rs)
406
- - nearby flow-parser projects such as the mirrored NetGauze parser benchmark by protocol and packet shape, which this implementation now mirrors operationally without introducing new bench dependencies
407
- - Implication:
408
- - the first pass is easy to run in CI-like environments with `cargo test --release ... --ignored --nocapture`
409
- - if we later need statistical benchmarking or regression thresholds, we can still add Criterion on top of the same scenarios
410
-- Decision made by Costa on `2026-04-11`: proceed autonomously with diagnosis and optimization of the remaining live memory footprint until the dominant owners are concretely attributed and reduced.
411
-- Decision pending on `2026-04-11`: whether to change the shared journal window-manager logic in [`src/crates/journal-core/src/file/mmap.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/mmap.rs) as part of the netflow memory work.
412
- - Context:
413
- - this logic is shared infrastructure, not netflow-only code
414
- - live evidence currently shows one large `rw-s` mapping per active tier journal file, which matches the current remap behavior in the shared window manager
415
- - changing shared journal infrastructure could reduce resident memory for netflow and any other current/future users of the same writer path, but it must be treated as an infrastructure fix, not a plugin-local tweak
416
-- If implementation reveals that the only viable 30x reduction requires changing query/facet behavior visible to users, I must stop and present options with concrete impact before proceeding.
417
-- Decision made by Costa on `2026-04-11`: proceed autonomously with the recommended attribution path for the live `~700 MB` RSS case:
418
- - inspect the live allocator/process footprint first
419
- - expand the plugin's internal accounting for major in-memory subsystems
420
- - use stress-harness profiling to map large allocations back to code paths if the live allocator/process view is not sufficient
421
-- Decision made by Costa on `2026-04-11`: choose the implementation and tooling path autonomously using best practices. Do not escalate Rust/allocator implementation details back to Costa unless there is a real product-level tradeoff. Allocator replacement is allowed if evidence shows it materially improves accounting or production memory behavior, but it must not be used blindly to hide ownership bugs.
422
-- Operational decision made by Costa on `2026-04-11`: when Netdata service restarts are needed on this workstation, use `sudo` so the authentication flow is explicit and does not rely on a background privilege escalation path.
423
-- Decision made autonomously on `2026-04-11` after direct benchmark evidence: keep the plugin on glibc and do not switch to mimalloc, jemalloc, or tcmalloc for this workload.
424
- - Evidence from the release startup harness on the retained live dataset:
425
- - glibc with `MALLOC_ARENA_MAX=1`: `rebuild_settle_trim delta_rss = 45,494,272`
426
- - mimalloc (`LD_PRELOAD=/usr/lib/libmimalloc.so`): `99,500,032`
427
- - jemalloc (`LD_PRELOAD=/usr/lib/libjemalloc.so`): `303,493,120`
428
- - tcmalloc (`LD_PRELOAD=/usr/lib/libtcmalloc_minimal.so`): `278,237,184`
429
- - Implication:
430
- - allocator replacement is not the right optimization path here; the right path is reducing transient allocations and capping glibc arena growth inside the process.
431
-- Decision made by Costa on `2026-04-11`: remove exact timestamp-microsecond fields such as `FLOW_START_USEC`, `FLOW_END_USEC`, and `OBSERVATION_TIME_MILLIS` from the facet/query catalog entirely.
432
- - Context:
433
- - the field is currently configured as `SparseU64`, `supports_autocomplete = true`, and `uses_sidecar = true` in [`src/crates/netdata-netflow/netflow-plugin/src/facet_catalog.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_catalog.rs)
434
- - archived facet autocomplete/search uses sidecars when a field is marked this way in [`src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs)
435
- - a targeted allocative diagnostic on the live dataset showed archived facet memory of `40,524,572` bytes total, of which `FLOW_END_USEC` alone consumed `32,263,968` bytes
436
- - Implication:
437
- - these fields will no longer be available as facet fields, autocomplete targets, or selection/filter fields for user requests
438
- - internal chart logic that reads `FLOW_END_USEC` directly from records remains unaffected
439
- - Implemented on `2026-04-11`:
440
- - removed from facet catalog and from query request allowlists in [`src/crates/netdata-netflow/netflow-plugin/src/facet_catalog.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_catalog.rs) and [`src/crates/netdata-netflow/netflow-plugin/src/query/request/constants.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/query/request/constants.rs)
441
- - added regressions for unsupported `group_by`, `selection`, and autocomplete requests in [`src/crates/netdata-netflow/netflow-plugin/src/query/tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/query/tests.rs)
442
- - verified with `cargo test -p netflow-plugin`: `378 passed, 0 failed, 14 ignored`
443
-- Decision made by Costa on `2026-04-12`: base per-tier journal rotation size on the per-tier total byte-retention budget, not on a fixed global file size.
444
- - Rule:
445
- - for per-tier `size_of_journal_files >= 100MB`, derive `size_of_journal_file = clamp(size_of_journal_files / 20, 5MB, 200MB)`
446
- - per-tier total byte-retention budgets below `100MB` are considered invalid for `netflow-plugin`
447
- - Intended operational effect:
448
- - once a tier is at steady-state retention, one full-file deletion should represent at most about `1/20` of the tier's byte history until the `200MB` cap is reached
449
- - above `4GB` per tier, the file-size cap keeps the rotation unit at `200MB`, so the fraction of history replaced on each steady-state rotation becomes even smaller than `1/20`
450
- - Critical implementation constraint verified in code:
451
- - the current plugin still enforces `number_of_journal_files` as an active retention limit in [`src/crates/netdata-netflow/netflow-plugin/src/ingest/service/init.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/service/init.rs) and [`src/crates/journal-log-writer/src/log/chain.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/chain.rs)
452
- - the current default of `64` files in [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/journal.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/journal.rs) would conflict with this rule for larger byte budgets unless that file-count limit is also derived, raised, or disabled by default
453
-- Decision made by Costa on `2026-04-12`: users must not configure `number_of_journal_files` directly.
454
- - Desired user-facing contract:
455
- - users configure per-tier retention by `size_of_journal_files` and/or `duration_of_journal_files`
456
- - the plugin derives any internal file-count limit needed to satisfy that contract
457
- - if the journal layer already supports size/time retention directly, the plugin should not keep an independent user-visible file-count knob
458
- - Verified library capability:
459
- - `journal-log-writer` retention already supports optional file-count, total-size, and age limits independently in [`src/crates/journal-log-writer/src/log/config.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/config.rs)
460
- - because those retention limits are optional, the plugin can satisfy size-only, time-only, or size+time retention without exposing `number_of_journal_files` to users
461
- - Implication:
462
- - the current plugin config model and validation in [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/journal.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/journal.rs) and [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/validation/journal.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/validation/journal.rs) need redesign before implementation, because they currently require `number_of_journal_files`, `size_of_journal_files`, and `duration_of_journal_files` all to be present and non-zero
463
-- Decision made by Costa on `2026-04-12`: for tiers that use time-only retention and do not define `size_of_journal_files`, start with a fixed internal rotation size of `100MB`.
464
- - Context:
465
- - rotation uses `size_of_journal_file` and/or `duration_of_journal_file`, and the active file rotates when any configured rotation limit is exceeded
466
- - retention deletion itself can already be time-only in [`src/crates/journal-log-writer/src/log/config.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/config.rs)
467
- - the current plugin default still hardcodes `size_of_journal_file = 256MB` and `duration_of_journal_file = 1h` in [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/journal.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/journal.rs)
468
- - because active journal residency is sensitive to file size, time-only retention still needs a bounded default rotation size even without a byte-retention budget
469
- - Additional feasibility constraint verified on `2026-04-12`:
470
- - true adaptive per-file sizing would require a small shared-writer change, because `journal-log-writer::Log` snapshots `size_of_journal_file` into `RotationState` at construction time in [`src/crates/journal-log-writer/src/log/mod.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/mod.rs)
471
- - there is currently no public API to update `rotation_policy.size_of_journal_file` or rebuild `RotationState` on an existing `Log`
472
- - Decision details:
473
- - use a fixed internal `100MB` rotation size for time-only tiers now
474
- - do not change `journal-log-writer` for adaptive rotation sizing in this pass
475
- - revisit adaptive sizing later only if real users ask for it
476
- - Implementation constraint found and fixed during validation on `2026-04-12`:
477
- - per-tier overrides need a tri-state model to distinguish:
478
- - omitted field => inherit global limit
479
- - explicit `null` => disable inherited limit
480
- - concrete value => override global limit
481
- - the first draft used plain `Option`, which could not represent explicit `null` for tier overrides correctly
482
- - the config model was updated to preserve this contract before claiming the refactor complete
483
-
484
-## Plan
485
-
486
-- Review the PR diff against `origin/master` and isolate every changed file outside `src/crates/netdata-netflow/netflow-plugin`.
487
-- For each shared-component change:
488
- - verify what behavior it changes in code
489
- - verify why it was introduced
490
- - decide whether it is necessary, optional, or should be reverted from the PR
491
- - record risks, especially shared-runtime or journal-layer blast radius
492
-- Present Costa with a review-style summary focused on findings first:
493
- - unnecessary shared changes
494
- - risky shared changes
495
- - shared changes that are justified and should remain
496
-- Cleanup implementation for the clearly unnecessary shared changes before performance work:
497
- - keep shared runtime generic by moving netflow request translation into the netflow handler
498
- - remove unused shared `journal-engine` builder surface that has no netflow production caller
499
- - revert duplicated `jf`-crate changes that are outside the netflow path
500
- - rerun the affected shared and plugin test suites before any performance work begins
501
-- After the shared-component review, design a performance test strategy for flow ingestion:
502
- - synthetic benchmark coverage for NetFlow, sFlow, and IPFIX
503
- - real fixture replay coverage where available
504
- - throughput and memory measurement dimensions
505
- - cardinality and field-variability scenarios
506
-- Implemented on `2026-04-12`:
507
- - split ingest test support out of oversized [`src/crates/netdata-netflow/netflow-plugin/src/ingest_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest_tests.rs) into dedicated support and benchmark modules
508
- - added protocol-specific release benchmarks in [`src/crates/netdata-netflow/netflow-plugin/src/ingest_bench_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest_bench_tests.rs):
509
- - NetFlow v5
510
- - NetFlow v9
511
- - IPFIX
512
- - sFlow
513
- - added a second benchmark matrix for post-decode ingest sensitivity to field variability/cardinality:
514
- - low-cardinality
515
- - medium-cardinality (`256` buckets)
516
- - high-cardinality (`50,000` unique buckets)
517
- - extracted a single-record ingest helper from [`src/crates/netdata-netflow/netflow-plugin/src/ingest/service/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest/service/runtime.rs) so the benchmark exercises the same raw-write/facet/tier path as the production packet handler
518
-- Verified on `2026-04-12`:
519
- - `cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml` passed with `388 passed, 0 failed, 15 ignored`
520
- - `cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml --release bench_ingestion_protocol_matrix -- --ignored --nocapture` passed
521
- - `cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml --release bench_ingestion_cardinality_matrix -- --ignored --nocapture` passed
522
-- First measured throughput on this workstation with the current release harness:
523
- - protocol matrix:
524
- - NetFlow v5 full ingest: about `49.6k flows/s`
525
- - NetFlow v9 full ingest: about `47.6k flows/s`
526
- - IPFIX full ingest: about `62.7k flows/s`
527
- - sFlow full ingest: about `45.7k flows/s`
528
- - decode-only throughput is much higher (`0.75M - 5.2M flows/s` depending on protocol), so the dominant steady-state cost is post-decode ingest, not wire parsing
529
- - post-decode cardinality matrix:
530
- - low-cardinality: about `53.4k flows/s`
531
- - medium-cardinality (`256` buckets): about `47.6k flows/s`
532
- - high-cardinality (`50,000` unique buckets): about `29.3k flows/s`
533
- - operational implication:
534
- - with the current single-threaded ingest path on this host, a reasonable expectation is roughly `45k - 63k flows/s` on warmed real fixtures at low-to-medium cardinality
535
- - high-cardinality post-decode ingest drops to about `29k flows/s`, which is the more realistic planning number when users have highly variable endpoint/exporter/interface dimensions
536
-- Re-run the full affected test suites after each memory change:
537
- - `cargo test -p journal-core -p journal-log-writer -p journal-engine -p netflow-plugin`
538
-- Improve facet runtime accounting first, because it is now a proven blind spot:
539
- - replace rough `HashTable` byte heuristics with real allocation reporting where the container exposes it
540
- - use a better size estimate for `RoaringTreemap`
541
- - include missing container-structure overhead for the facet `BTreeMap`/published snapshot bookkeeping
542
-- Re-run the focused facet harness after the estimator changes and compare:
543
- - actual RSS growth
544
- - facet-accounted bytes
545
- - remaining gap
546
-- Use the improved live charts to decide the next optimization target instead of guessing.
547
-- New verified optimization target from Massif on `2026-04-11`:
548
- - `TextValueStore::insert()` appears twice from `apply_active_contribution()`, once for the per-file contribution store and once for the global `active_fields` union.
549
- - This means new active text values are stored twice in long-lived runtime memory.
550
- - `DenseBitSet::new()` also shows measurable startup allocation through `empty_field_stores()`, even before those dense stores hold any values.
551
-- Next implementation step:
552
- - remove the persistent `active_fields` duplicate store and derive active unions from `active_contributions` when needed
553
- - make dense facet bitsets allocate lazily on first insert instead of at empty-store construction
554
- - then rerun the full `netflow-plugin` suite and the focused facet-memory harness
555
-- Additional verified next target:
556
- - archived facet startup load still costs about `48.3 MB` RSS even after `active=0` and `active_contrib=0`
557
- - the current persisted text facet format is `Vec<String>`, which means startup deserializes many heap strings and then rebuilds compact text stores from them
558
- - the next optimization pass should persist/load text stores in compact arena form so startup can rebuild the lookup index without first materializing `Vec<String>`
559
-- New verified next target on `2026-04-11`:
560
- - the remaining live resident gap is dominated by allocator-retained free heap, not by GeoIP/MMDB data or by a better general-purpose allocator existing on this host
561
- - the next optimization pass should therefore focus on reducing transient startup allocations further and on improving charts so allocator-retained heap is shown explicitly instead of being hidden inside `unaccounted`
562
-- New verified next target on `2026-04-11` after allocative archived-facet profiling:
563
- - the current archived facet estimate is materially wrong for `SparseU64` timestamp-like fields
564
- - `FLOW_END_USEC` is the dominant verified archived-facet memory owner and should be treated as the next major optimization decision
565
-- New verified next target on `2026-04-11` after the latest live chart review:
566
- - add a true process-side resident mapping breakdown chart from `/proc/self/smaps`
567
- - classify resident bytes into disjoint buckets such as:
568
- - heap
569
- - anonymous non-heap mappings
570
- - raw journal mmaps
571
- - `1m` journal mmaps
572
- - `5m` journal mmaps
573
- - `1h` journal mmaps
574
- - other file-backed mappings
575
- - shmem
576
- - this will not replace internal heap ownership accounting, but it will stop forcing most of RSS into an opaque `unaccounted` bucket when the kernel already exposes the owning mapping classes
577
-- New verified next target on `2026-04-11` after correlating live mappings with enrichment assets:
578
- - switch GeoIP database loading from eager copied `Reader<Vec<u8>>` to mmap-backed readers
579
- - extend the resident mapping chart to break GeoIP MMDB residency out separately from generic `other_file`
580
- - this should remove about `142 MB` of anonymous copied database memory while preserving enrichment behavior
581
- - status update after install:
582
- - implemented and verified live
583
- - current remaining blind spot is no longer a huge anonymous GeoIP owner; it is the smaller steady-state heap / runtime path that the existing startup harness still does not exercise
584
-
585
-1. Build a local stress environment around the real `netflow-plugin` binary/crate using isolated cache/config dirs and existing UDP replay helpers as the starting point.
586
-2. Add a reproducible load generator that can sustain much higher cardinality and volume than the office feed, so memory growth can be observed quickly and deterministically.
587
-3. Capture a baseline:
588
- - RSS / max RSS
589
- - ingest counters
590
- - journal growth
591
- - memory snapshots over time
592
- - allocator evidence where possible
593
-4. Attribute memory to concrete subsystems by selectively measuring with feature/code-path instrumentation and targeted profiling.
594
-5. Add internal memory accounting for the major in-memory subsystems and expose it via Netdata charts.
595
-6. Implement the highest-impact reductions first:
596
- - first, replace per-file facet string sets with compact typed stores and stop reparsing/recloning compact values into the resident path
597
- - then remove the duplicate combined facet store from runtime memory while preserving published facet behavior on restart
598
- - then add byte accounting for resident facet/tier/open-row structures and expose them as charts
599
- - then switch tier rollup rows to sparse/default-aware storage and compress IPv4-heavy IP dictionaries
600
- - then normalize decoder template scope away from raw UDP source-port churn if the parser path proves to be retaining duplicate template state per exporter IP
601
- - then re-measure and decide whether more aggressive high-cardinality facet residency reduction is still required
602
-7. Add tests/benchmarks so the regression is pinned in CI or at least in local automated verification.
603
-8. Re-run the same stress scenario and compare against baseline with exact numbers.
604
-9. Attribute the current live `~700 MB` RSS case by combining:
605
- - kernel/process evidence from `/proc/<pid>/status`, `/proc/<pid>/smaps`, and `/proc/<pid>/smaps_rollup`
606
- - allocator/process evidence from the live glibc-backed binary
607
- - plugin-side accounting for the largest resident runtime structures
608
-10. Replace the coarse `unaccounted` bucket with named subsystem buckets where the codebase permits reliable accounting, so field diagnosis in customer environments points to concrete owners instead of just total RSS.
609
-11. Prove the live writable-journal mmap growth in the shared `journal-core` window manager with a focused regression test, then fix that shared behavior if the test confirms full-file growth from sequential append access.
610
-12. Verify or reject the current GeoIP/MMDB hypothesis with direct evidence from:
611
- - the auto-detection code path in the plugin
612
- - the actual MMDB files present on this machine
613
- - the live allocator/process footprint
614
-13. If GeoIP/MMDB is a dominant owner:
615
- - add explicit accounting for it in the memory charts
616
- - decide whether eager `Vec<u8>` loading is acceptable or should be replaced with a lower-RSS access pattern
617
-14. Extend the startup profiling harness so it also exercises the real plugin-runtime startup path:
618
- - construct `PluginRuntime`
619
- - register the `flows` handler
620
- - register charts
621
- - start the runtime on in-memory streams long enough for chart/runtime initialization to happen
622
- - measure whether that path adds any meaningful steady-state heap beyond the already measured ingest/query/rebuild path
623
-15. Re-run the full relevant test suites after the runtime-harness change:
624
- - `cargo test -p journal-core -p journal-log-writer -p journal-engine -p netflow-plugin`
625
-16. Re-check the live installed process after the harness and chart work:
626
- - validate RSS / anonymous / file-backed split
627
- - validate `memory_resident_mapping_bytes`
628
- - validate whether `memory_accounted_bytes.unaccounted` is now small enough to be operationally useful
629
- - current reality:
630
- - the live mapping chart is now operationally useful and explains the majority of RSS in concrete buckets (`heap`, `geoip_geo`, `geoip_asn`, journal tiers, `other_file`)
631
- - the logical `memory_accounted_bytes` chart still keeps a smaller `unaccounted` bucket because it intentionally tracks named plugin subsystems, not every resident mapping class
632
-
633
-## Implied decisions
634
-
635
-- All stress work will stay inside this worktree and temp directories created under it or safe local temp space.
636
-- No production routers, remote servers, or non-project directories will be touched beyond local profiling tools and temporary test data.
637
-- The stress harness should be reusable for future memory regressions, not a one-off manual sequence.
638
-
639
-## Testing requirements
640
-
641
-- Baseline and post-fix stress runs on the same workload and same binary mode.
642
-- Automated test coverage for the specific memory-driving structure(s) changed.
643
-- Regression-oriented assertions where practical:
644
- - bounded facet growth
645
- - bounded per-hour rollup index growth
646
- - no loss of ingest/query correctness for representative flows
647
-- Verification that new memory accounting/charts track the intended subsystems and update during load.
648
-- Re-run relevant `netflow-plugin` tests after code changes.
649
-
650
-### Verification completed so far
651
-
652
-- `cargo test -p netflow-plugin charts::tests`
653
-- `cargo test -p netflow-plugin facet_runtime::tests`
654
-- `cargo test -p netflow-plugin tiering::tests`
655
-- `cargo test -p netflow-plugin query::tests`
656
-- `cargo test -p netdata-flow-index`
657
-- `cargo test -p netflow-plugin stress_profile_high_cardinality_facet_runtime_memory -- --ignored --nocapture`
658
-- `cargo test -p netflow-plugin stress_profile_high_cardinality_tier_index_memory -- --ignored --nocapture`
659
-- `cargo test -p netflow-plugin stress_profile_high_cardinality_netflow_memory -- --ignored --nocapture`
660
-- `cargo test -p netflow-plugin stress_profile_decoder_source_port_churn_memory -- --ignored --nocapture`
661
-- `cargo test -p netflow-plugin decoder::tests::v9_parser_scope_reuses_templates_across_source_port_churn -- --nocapture`
662
-- `cargo test -p netflow-plugin decoder::tests::persisted_decoder_state_reuses_loaded_namespace_for_new_source_port -- --nocapture`
663
-- `cargo test -p journal-engine`
664
-- `cargo test -p netflow-plugin`
665
-
666
-### Broader suite status
667
-
668
-- The earlier 4 broader-suite failures were root-caused and fixed in this worktree:
669
- - `decoder::tests::akvorado_sflow_vxlan_fixture_matches_expected_inner_projection`
670
- - `decoder::tests::akvorado_sflow_1140_fixture_matches_expected_sample_projection`
671
- - `enrichment::tests::enricher_is_disabled_when_configuration_is_empty`
672
- - `ingest::tests::ingest_service_with_decap_vxlan_extracts_inner_header_view`
673
-- Verified before the latest memory/rebuild commit:
674
- - `cargo test -p netflow-plugin`
675
- - result: `375 passed, 0 failed, 13 ignored`
676
-- New requirement on `2026-04-11`: re-run the relevant suites after the latest pushed commit and treat any new failure as a blocker before continuing the unresolved live memory investigation.
677
-
678
-### Verified root causes for the remaining test failures
679
-
680
-- Empty enrichment config is not actually empty:
681
- - [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/enrichment/root.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/types/enrichment/root.rs) makes `EnrichmentConfig::default()` include non-empty `asn_providers` and `net_providers`.
682
- - [`src/crates/netdata-netflow/netflow-plugin/src/enrichment/init.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/enrichment/init.rs) treats any non-empty provider list as sufficient to enable enrichment.
683
- - Result: [`src/crates/netdata-netflow/netflow-plugin/src/enrichment/tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/enrichment/tests.rs) `enricher_is_disabled_when_configuration_is_empty()` fails because `EnrichmentConfig::default()` can no longer represent a disabled configuration.
684
-- IPv4/IPv6 packet-section length accounting is clamping to captured bytes instead of using the on-wire L3 length encoded in the packet headers:
685
- - [`src/crates/netdata-netflow/netflow-plugin/src/decoder/protocol/packet/ip.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/decoder/protocol/packet/ip.rs) returns `captured_length` / `payload_end`.
686
- - [`src/crates/netdata-netflow/netflow-plugin/src/decoder/record/packet/parse/ip.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/decoder/record/packet/parse/ip.rs) mirrors the same clamp in the `FlowRecord` path.
687
- - [`src/crates/netdata-netflow/netflow-plugin/src/decoder/protocol/sflow/record.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/decoder/protocol/sflow/record.rs) then uses that returned length as `rec.bytes`.
688
- - Reproduced failures:
689
- - `data-encap-vxlan.pcap`: expected `BYTES=104`, actual `BYTES=64`
690
- - `data-1140.pcap`: expected visible `BYTES=1536000`, actual `BYTES=116736`, which means raw `114 * 1024` instead of `1500 * 1024`
691
- - Upstream Akvorado evidence:
692
- - [`/opt/baddisk/monitoring/akvorado/akvorado__akvorado/outlet/flow/decoder/sflow/root_test.go`](/opt/baddisk/monitoring/akvorado/akvorado__akvorado/outlet/flow/decoder/sflow/root_test.go) expects `1500` for `data-1140.pcap` and `104` for `data-encap-vxlan.pcap`.
693
- - [`/opt/baddisk/monitoring/akvorado/akvorado__akvorado/outlet/flow/decoder/helpers.go`](/opt/baddisk/monitoring/akvorado/akvorado__akvorado/outlet/flow/decoder/helpers.go) returns the IPv4/IPv6 header-declared L3 length, not the truncated capture size.
694
-
695
-### Open reality check
696
-
697
-- The work is not yet at the requested end state.
698
-- The facet hotspot is materially better and now observable via charts/accounting.
699
-- The tier index hotspot is materially better and now exposes an internal breakdown via charts/accounting.
700
-- The decoder template/parser churn case is now both observable and fixed for same-exporter source-port churn, with a measured `~430x` reduction on the synthetic churn harness.
701
-- The measured focused reductions are real:
702
- - facet hotspot RSS delta: `21.24 MB -> 8.77 MB`
703
- - tier hotspot RSS delta: `22.21 MB -> 9.90 MB`
704
- - decoder source-port churn hotspot RSS delta: `867.91 MB -> 2.02 MB`
705
-- These measured results support a `>30x` reduction only for the verified decoder source-port churn failure mode. They still do not justify claiming an overall end-to-end `30x` reduction for all workloads.
706
-- Live measurement on `2026-04-11` shows the currently installed plugin at about `674 MiB` RSS (`706,560,000` bytes from the chart; `~690-708 MB` from `/proc` depending on sampling time).
707
-- The live process is dominated by private anonymous memory:
708
- - `RssAnon`: about `660 MB`
709
- - `RssFile`: about `30-48 MB`
710
- - largest mappings: `[heap]` about `361 MB`, `[anon]` about `299 MB`
711
-- The current charted accounting does not explain the live footprint yet:
712
- - `netdata.netflow.memory_accounted_bytes.unaccounted`: `692,828,350` bytes
713
- - all explicitly named buckets combined: about `13.1 MB`
714
- - conclusion: the chart framework works, but it still lacks attribution for the dominant live heap owners in this case
715
-- Live allocator introspection through `gdb` has a verified safety boundary:
716
- - attaching and detaching from the running process is safe on this machine
717
- - resolving glibc allocator symbols (`malloc_info`, `mallinfo2`) is also possible
718
- - calling `malloc_info()` from inside the live process is not safe in this environment: it faulted inside glibc `fputs()` while writing the allocator report and the plugin PID disappeared afterwards
719
- - this means "live allocator dump via injected `gdb` call" is no longer an acceptable primary method for this process
720
-
721
-## New obstacle
722
-
723
-- `gdb`-injected allocator reporting is not safe enough for the running plugin.
724
-- Verified evidence:
725
- - `gdb` attach/detach works and shared-library symbol resolution can find `malloc_info` / `mallinfo2`.
726
- - forcing `malloc_info()` inside the live process caused a fault in glibc while executing `fputs()`.
727
- - the inspected plugin PID (`1164472`) disappeared after that attempt.
728
-- Implication:
729
- - allocator attribution must move to one of these safer paths:
730
- - an in-process diagnostic endpoint/chart implemented in the plugin itself
731
- - an isolated stress-harness build with allocator tracing/profiling enabled
732
- - offline process/core analysis instead of injected live function calls
733
-- The broader `netflow-plugin` suite was green before commit `d6e4ff75c0`, but it must be re-validated after that commit before more optimization work continues.
734
-- Re-validated after commit `d6e4ff75c0`:
735
- - `cargo test -p journal-engine`: passed
736
- - `cargo test -p netflow-plugin`: passed
737
- - `netflow-plugin` result: `375 passed, 0 failed, 13 ignored`
738
-
739
-### New live-memory findings after test revalidation on 2026-04-11
740
-
741
-- The current running plugin (`/usr/libexec/netdata/plugins.d/netflow-plugin`, PID observed as `1356956`) is now dominated by both anonymous and file-backed resident memory:
742
- - `VmRSS`: about `623,908 kB`
743
- - `RssAnon`: about `355,928 kB`
744
- - `RssFile`: about `267,980 kB`
745
- - `Threads`: `14`
746
-- The live process currently holds four very large writable shared journal mappings:
747
- - raw journal mapping: about `111,668 kB RSS`
748
- - `1m` journal mapping: about `77,344 kB RSS`
749
- - `5m` journal mapping: about `53,480 kB RSS`
750
- - `1h` journal mapping: about `19,552 kB RSS`
751
- - Evidence: `/proc/<pid>/maps` and `pmap -x` show large `rw-s` mappings for the active journal files in `raw`, `1m`, `5m`, and `1h`.
752
-- The same live process also still holds dozens of open file descriptors to the rebuild index cache directory:
753
- - `/var/cache/netdata/flows/.rebuild-index-cache/foyer-storage-direct-fs-*`
754
- - Evidence: `/proc/<pid>/fd`
755
- - This means the rebuild-scoped Foyer cache is not being closed cleanly after startup.
756
-- The release startup harness with the current code and the same local journal base directory does not reproduce the full live file-backed shape:
757
- - `mmap_in_use` peaks at about `33.6 MB`
758
- - rebuild settle RSS is about `156.9 MB`
759
- - conclusion: the remaining live RSS is not just startup rebuild anymore
760
-- The journal window manager implementation in [`src/crates/journal-core/src/file/mmap.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/mmap.rs) has a likely structural bug for sequential appends:
761
- - when a request starts inside an existing window but extends beyond its end, `get_window()` remaps from the existing window's original start instead of aligning a fresh bounded tail window to the current position
762
- - this can grow one mapping from the beginning of the file toward the current write position over time
763
- - this behavior matches the observed one-large-map-per-active-tier pattern in the live process
764
-
765
-### Verified diagnosis of the large live journal mmaps on 2026-04-11
766
-
767
-- The suspicious shared remap logic in [`src/crates/journal-core/src/file/mmap.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/mmap.rs) was **not** introduced by this PR:
768
- - `git blame` on the remap branch (`window_start = window.offset`) points to commit `adc4d66ea03` from `2025-05-28`
769
- - the only newer changes around that code are the later consistency guards for mmap failure handling
770
-- The `journal-log-writer` active writer window size of `8 MiB` was also not changed by this PR:
771
- - `git blame` on [`src/crates/journal-log-writer/src/log/mod.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-log-writer/src/log/mod.rs) shows `.with_window_size(8 * 1024 * 1024)` coming from commit `d0905d9b99`
772
-- The shared journal code explicitly documents the intended design:
773
- - [`src/crates/journal-core/src/file/file.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/file.rs) says the implementation should maintain a **small set of memory-mapped windows** instead of mapping the entire file
774
-- Live process evidence contradicts that intended design for the active writable journals:
775
- - current active file sizes:
776
- - raw: `152 MiB`
777
- - `1m`: `128 MiB`
778
- - `5m`: `72 MiB`
779
- - `1h`: `24 MiB`
780
- - current main `rw-s` mapping sizes for those same files:
781
- - raw: `152 MiB`
782
- - `1m`: `128 MiB`
783
- - `5m`: `72 MiB`
784
- - `1h`: `24 MiB`
785
- - conclusion: the main writable mapping size matches the full current file size, not the configured `8 MiB` writer window
786
-- The always-mapped journal hash tables are **not** the explanation:
787
- - for a `256 MiB` max file size, the persistent data-hash table is about `1 MiB`
788
- - the field-hash table is negligible (`~2 KiB`)
789
- - therefore the large `24-152 MiB` mappings are not expected fixed metadata maps
790
-- The most likely overall explanation is now:
791
- - the shared journal window-manager behavior is pre-existing
792
- - this PR exposed it operationally by creating a workload with four continuously written active journals (`raw`, `1m`, `5m`, `1h`) and large active file sizes
793
- - the recent PR changes in `journal-log-writer` themselves are timestamp/lifecycle additions, not mmap-window behavior changes
794
-
795
-### Live revalidation after fixing the shared journal window manager on 2026-04-11
796
-
797
-- After installing the bounded-window fix from [`src/crates/journal-core/src/file/mmap.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/mmap.rs), the live process no longer maps whole active journal files:
798
- - live `raw` writer map: `8 MiB`
799
- - live `1m` writer map: `8 MiB`
800
- - the extra `~1 MiB` maps per file remain, which matches expected fixed per-file metadata mappings
801
-- Live resident memory after restart materially improved:
802
- - `/proc/<pid>/status` showed `RssFile` collapse from about `268 MiB` before the fix to about `19-28 MiB` after the fix
803
- - the same live process stabilized around `VmRSS ≈ 350-408 MiB`, `RssAnon ≈ 331-384 MiB`, `Threads = 14`
804
-- Netdata's own live charts now agree with `/proc`:
805
- - `netdata.netflow.memory_resident_bytes` current samples show `rss ≈ 406-408 MiB`, `rss_anon ≈ 383.6 MiB`, `rss_file ≈ 22.7-25.0 MiB`
806
- - `netdata.netflow.memory_accounted_bytes` current samples still show `unaccounted ≈ 393-395 MiB`
807
- - `netdata.netflow.memory_allocator_bytes` current samples show:
808
- - `heap_in_use ≈ 66.97 MiB`
809
-
810
-### Verified allocator-retention diagnosis and post-fix live state on 2026-04-11
811
-
812
-- GeoIP/MMDB data is not a dominant owner in this environment:
813
- - auto-detection uses [`src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/plugin_config/runtime.rs) and eager loading uses [`src/crates/netdata-netflow/netflow-plugin/src/enrichment/data/geoip/files.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/enrichment/data/geoip/files.rs)
814
- - actual detected files on this machine:
815
- - `/var/cache/netdata/topology-ip-intel/topology-ip-asn.mmdb`: `10,402,916` bytes
816
- - `/var/cache/netdata/topology-ip-intel/topology-ip-country.mmdb`: `5,875,413` bytes
817
- - combined resident upper bound is only about `16.3 MB`, so MMDB files cannot explain the remaining `~200+ MB` anonymous RSS
818
-- The allocator chart's `mmap_in_use` value was re-checked and must not be treated as resident RAM by itself:
819
- - [`src/crates/netdata-netflow/netflow-plugin/src/memory_allocator.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/memory_allocator.rs) reports `mallinfo2().hblkhd`
820
- - `/proc/<pid>/smaps` still shows the dominant live resident memory in anonymous heap/arena mappings, not in a separate resident mapping equal to `mmap_in_use`
821
- - implication: `mmap_in_use` is allocator state, not a direct resident-memory answer
822
-- Correct live process before the latest allocator-focused fixes:
823
- - observed plugin PID: `1699732`
824
- - `/proc/<pid>/status`:
825
- - `VmRSS: 362,188 kB`
826
- - `RssAnon: 320,984 kB`
827
- - `RssFile: 41,204 kB`
828
- - `Threads: 9`
829
- - `/proc/<pid>/smaps_rollup`:
830
- - `Private_Dirty: 320,984 kB`
831
- - `AnonHugePages: 0 kB`
832
- - top resident mappings:
833
- - anonymous: `146,000 kB`
834
- - `[heap]`: `82,912 kB`
835
- - anonymous: `39,032 kB`
836
- - anonymous: `33,164 kB`
837
- - anonymous: `18,784 kB`
838
-- The dominant remaining mechanism was verified as allocator-retained free heap:
839
- - live allocator chart before the latest code change showed approximately:
840
- - `heap_arena ≈ 500,064,300`
841
- - `heap_free ≈ 433,473,010`
842
- - `heap_in_use ≈ 66,591,236`
843
- - conclusion:
844
- - most anonymous heap footprint was not live payload; it was free heap retained by glibc after earlier startup/runtime allocation spikes
845
-- The service environment already had `MALLOC_ARENA_MAX=4`, but reducing it further to `1` still helped on the real startup workload:
846
- - release multithread harness on the retained live dataset:
847
- - `MALLOC_ARENA_MAX=4`: `rebuild_settle_trim delta_rss = 80,465,920`
848
- - `MALLOC_ARENA_MAX=1`: `rebuild_settle_trim delta_rss = 47,730,688`
849
- - verified reduction: about `32.7 MB`
850
-- Fix implemented in [`src/crates/netdata-netflow/netflow-plugin/src/memory_allocator.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/memory_allocator.rs) and wired from [`src/crates/netdata-netflow/netflow-plugin/src/main.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/main.rs):
851
- - call `mallopt(M_ARENA_MAX, 1)` inside the plugin process before Tokio runtime creation
852
- - keep THP disabled for the process
853
-- Additional startup-allocation reduction implemented in [`src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/facet_runtime.rs):
854
- - load persisted facet state via `BufReader + bincode::deserialize_from()` instead of `fs::read()`
855
- - consume persisted archived stores directly instead of cloning every saved store during hydration
856
-- Verified live state after installing the arena-cap build:
857
- - observed plugin PID: `1728342`
858
- - `/proc/<pid>/status`:
859
- - `VmRSS: 227,880 kB`
860
- - `RssAnon: 213,344 kB`
861
- - `RssFile: 14,536 kB`
862
- - `Threads: 9`
863
- - allocator chart:
864
- - `heap_arena ≈ 326,135,800`
865
- - `heap_free ≈ 261,295,660`
866
- - `heap_in_use ≈ 64,840,148`
867
- - `mmap_in_use ≈ 131,457,140`
868
- - `unaccounted ≈ 225,923,660`
869
- - verified improvement versus the earlier `362,188 kB` live RSS: about `134 MB` lower
870
-- Verified live state after also installing the persisted-load cleanup:
871
- - observed plugin PID: `1743390`
872
- - `/proc/<pid>/status`:
873
- - `VmRSS: 227,840 kB`
874
- - `RssAnon: 211,636 kB`
875
- - `RssFile: 16,204 kB`
876
- - `Threads: 9`
877
- - allocator chart:
878
- - `heap_arena ≈ 313,335,800`
879
- - `heap_free ≈ 260,037,540`
880
- - `heap_in_use ≈ 53,298,250`
881
- - `mmap_in_use ≈ 143,175,840`
882
- - `unaccounted ≈ 222,713,120`
883
- - conclusion:
884
- - the persisted-load cleanup helped slightly, but the major win came from capping glibc arena growth inside the process
885
-
886
-### Verified archived facet deep-size diagnosis on 2026-04-11
887
-
888
-- A targeted allocative diagnostic was added and executed against the live dataset:
889
- - `cargo test -p netflow-plugin stress_profile_live_archived_facet_allocative_breakdown -- --ignored --nocapture`
890
-- Results on the current `facet-state.bin`:
891
- - archived facet memory by deep allocative traversal: `40,524,572` bytes
892
- - archived path tracking: `38,026` bytes
893
- - published snapshot: `5,844` bytes
894
-- Top archived facet fields by measured deep allocation:
895
- - `FLOW_END_USEC`: `32,263,968` bytes
896
- - `DST_ADDR_NAT`: `1,564,840`
897
- - `DST_ADDR`: `1,564,768`
898
- - `SRC_ADDR`: `1,560,312`
899
- - `SRC_ADDR_NAT`: `1,560,304`
900
- - `DST_AS_NAME`: `589,824`
901
- - `SRC_AS_NAME`: `589,824`
902
-- This diagnostic also proved the current estimator is materially wrong for that dominant field:
903
- - `FLOW_END_USEC` current estimated bytes: `5,456,148`
904
- - `FLOW_END_USEC` measured deep bytes: `32,263,968`
905
- - undercount factor: about `5.9x`
906
-- Interpretation:
907
- - the current facet chart under-accounting is not generic hand-waving anymore; it now has a named dominant owner
908
- - the dominant owner is a timestamp-microsecond facet that is currently being treated as an archived/autocomplete/searchable dimension like a normal categorical facet
909
-- Follow-up verification after Costa approved removal of those fields:
910
- - `cargo test -p netflow-plugin stress_profile_live_archived_facet_allocative_breakdown -- --ignored --nocapture`
911
- - current archived facet memory by deep allocative traversal: `8,260,553` bytes
912
- - current top archived fields are now IP and ASN/name dimensions:
913
- - `DST_ADDR_NAT`: `1,564,840`
914
- - `DST_ADDR`: `1,564,768`
915
- - `SRC_ADDR`: `1,560,312`
916
- - `SRC_ADDR_NAT`: `1,560,304`
917
- - `DST_AS_NAME`: `589,824`
918
- - `SRC_AS_NAME`: `589,824`
919
- - conclusion:
920
- - removing the internal timestamp fields produced a measured `~4.9x` reduction in archived facet deep size on the same dataset (`40.5 MB -> 8.3 MB`)
921
- - the next archived facet owners are IP dictionaries, not hidden timestamp sidecars
922
- - `heap_free ≈ 604.41 MiB`
923
- - `heap_arena ≈ 671.38 MiB`
924
- - `mmap_in_use ≈ 165.03 MiB`
925
- - `releasable ≈ 4 KiB`
926
-- This is a strong allocator-fragmentation/retention signal:
927
- - the live process currently has far more free heap retained inside glibc arenas than live heap objects
928
- - `malloc_trim()` by itself is unlikely to solve the remaining problem because the allocator reports only `~4 KiB` releasable despite `~604 MiB` free inside arenas
929
-- A release startup harness run against the same live journal corpus no longer reproduces the current live RSS:
930
- - `cargo test -p netflow-plugin --release stress_profile_live_startup_memory_multithreaded -- --ignored --nocapture`
931
- - result after settle+trim: `rss ≈ 97.8 MiB`, `heap_free ≈ 324.9 MiB`, `heap_arena ≈ 379.5 MiB`, `threads = 7`
932
- - implication: pure startup rebuild is no longer sufficient to explain the live `~400 MiB` steady-state footprint; the remaining growth is tied to runtime behavior while the service is active
933
-- A new verified runtime-lifetime bug remains:
934
- - the live process still keeps `64` open file descriptors under `/var/cache/netdata/flows/.rebuild-index-cache/foyer-storage-direct-fs-*` long after rebuild should have finished
935
- - this indicates the Foyer rebuild index cache must be closed explicitly instead of relying on drop timing
936
- - source review of `foyer-storage` shows why `close()` alone is insufficient: the block-engine `close()` path stops writers and waits for reclaim, but the direct-fs partitions keep their `File` handles in `FsPartition.file` until the storage device itself is dropped
937
- - because the rebuild cache is only scratch state for one rebuild run, the safer memory-first fix is to stop using disk-backed Foyer storage for this path and keep the cache in memory only
938
-- Allocator-substitution diagnostics were run against the same startup profile and rejected:
939
- - glibc baseline settle+trim: `~97.8 MiB RSS`
940
- - `mimalloc` preload settle: `~252.3 MiB RSS`
941
- - `jemalloc` preload settle: `~446.4 MiB RSS`
942
- - glibc with transparent huge pages disabled settle+trim: `~138.2 MiB RSS`
943
- - conclusion: allocator replacement and THP disablement are not the right first-line fix for this workload
944
-
945
-### Verified shared journal mmap growth diagnosis on 2026-04-11
946
-
947
-- The shared remap branch in `origin/master` was inspected with:
948
- - `git show origin/master:src/crates/journal-core/src/file/mmap.rs | sed -n '200,260p'`
949
- - verified old logic:
950
- - remove current window
951
- - keep `window_start = window.offset`
952
- - extend only `window_end` to cover the new request
953
-- That means append-heavy accesses crossing the current window boundary can grow a single writable mapping from the beginning of the file toward the tail instead of sliding a bounded window.
954
-- The current worktree contains a focused regression in [`src/crates/journal-core/src/file/mmap.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/journal-core/src/file/mmap.rs#L476):
955
- - `sequential_boundary_crossing_slides_window_instead_of_growing_from_start`
956
- - it verifies a `max_windows=1` manager with `4 KiB` chunks:
957
- - first cross-boundary remap produces a `2`-chunk window at offset `0`
958
- - the next cross-boundary remap slides to offset `4096` instead of growing to `12288` bytes from offset `0`
959
- - the next remap slides again to offset `8192`
960
-- Verified test evidence:
961
- - `cargo test -p journal-core sequential_boundary_crossing_slides_window_instead_of_growing_from_start -- --nocapture`
962
- - `cargo test -p journal-core`
963
- - `cargo test -p journal-log-writer`
964
- - `cargo test -p journal-engine`
965
- - `cargo test -p netflow-plugin`
966
- - all passed on the current worktree
967
-- Conclusion:
968
- - the large writable journal mmaps observed live are consistent with a real pre-existing shared `journal-core` remap-growth bug/design failure, not with new mmap logic introduced by this PR
969
- - `netflow-plugin` exposed it because it keeps several active tier writers growing concurrently
970
- - this shared fix is still uncommitted at the time of this TODO update
971
-- Live post-install snapshot after installing the current worktree and restarting Netdata:
972
- - observed plugin PID: `1808192`
973
- - `/proc/<pid>/status`:
974
- - `VmRSS: 181,204 kB` initially, `185,940 kB` after another minute
975
- - `RssAnon: 165,896 kB` initially, `166,268 kB` after another minute
976
- - `RssFile: 15,308 kB` initially, `19,672 kB` after another minute
977
- - `Threads: 9`
978
- - `/proc/<pid>/smaps_rollup` after the extra minute:
979
- - `Rss: 185,940 kB`
980
- - `Private_Dirty: 168,508 kB`
981
- - `Private_Clean: 14,944 kB`
982
- - `AnonHugePages: 0 kB`
983
- - active journal mappings in `/proc/<pid>/maps`:
984
- - raw active journal mapped bytes: `9,449,472`
985
- - `1m` active journal mapped bytes: `9,449,472`
986
- - current active file sizes: `8,388,608` each
987
- - caveat:
988
- - this is a fresh-restart snapshot, so it is not a perfect long-runtime comparison to the earlier `24-152 MB` active full-file mappings
989
- - however it is consistent with the bounded-window fix and materially lower than the earlier live `227-362 MB` snapshots on this workstation
990
-
991
-### Performance-testing feasibility on 2026-04-12
992
-
993
-- `FEASIBLE AS SPECIFIED`
994
-- Existing benchmark and profiling surface already covers most of the path we need:
995
- - end-to-end ingest hot path replay already exists in [`src/crates/netdata-netflow/netflow-plugin/src/ingest_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/ingest_tests.rs) via `bench_full_hot_path()`
996
- - fixture replay helpers already exist in [`src/crates/netdata-netflow/netflow-plugin/src/main_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/main_tests.rs) via `ingest_fixture()` and `replay_fixture_udp()`
997
- - raw query stage profiling already exists in [`src/crates/netdata-netflow/netflow-plugin/src/query/scan/bench.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/query/scan/bench.rs)
998
- - memory stress harnesses already exist in [`src/crates/netdata-netflow/netflow-plugin/src/memory_tests.rs`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/src/memory_tests.rs)
999
-- Current gaps:
1000
- - the existing ingest hot-path benchmark mixes protocols together instead of reporting per-protocol throughput
1001
- - it currently covers NetFlow v5, NetFlow v9, and IPFIX fixture replay, but not sFlow in the same throughput harness
1002
- - it does not yet vary field variability or cardinality in a controlled way
1003
- - it does not emit a stable summary format suitable for repeated comparison
1004
-- First implementation path:
1005
- - keep the current ignored-test harness model
1006
- - extend `bench_full_hot_path()` or split it into protocol-specific ignored tests
1007
- - add protocol-isolated replay groups for NetFlow v5, NetFlow v9, IPFIX, and sFlow
1008
- - add synthetic high-cardinality flow generation using the existing stress-helper patterns so we can measure the effect of field variability/cardinality separately from decoder cost
1009
- - report at minimum:
1010
- - flows per second
1011
- - packets per second
1012
- - bytes per second
1013
- - microseconds per flow
1014
- - peak RSS / resident mapping snapshot when practical
1015
-
1016
-## Documentation updates required
1017
-
1018
-- Completed:
1019
- - updated [`src/crates/netdata-netflow/netflow-plugin/README.md`](/home/costa/src/PRs/topology-netflow-22111/src/crates/netdata-netflow/netflow-plugin/README.md) with the new memory chart names and their operational meaning, including decoder scope diagnostics for source-port churn.