You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
66% of a staging telemetry-usage refresh cycle is spent converting Flux rows in-process — more than the InfluxDB read and the ClickHouse insert combined. This is now the dominant term in whether ingest keeps up with real time, and it is a code question rather than a capacity one.
Full breakdown of one staging cycle (2026-08-06 14:35:24 → 14:46:08, mainnet-beta, indexer at 4 CPU):
Phase
Log boundary
Duration
Share
Baseline lookup
queried baselines from clickhouse (cache hit)
~0s (920ns)
0%
InfluxDB Flux read
→ main influxdb query completed (6,899,363 rows)
2m32s
24%
Row conversion
→ queried influxdb (4,611,619 records)
7m04s
66%
ClickHouse insert
→ inserted data to clickhouse
1m07s
10%
Total
refresh completed
10m43s
That middle phase is sort.Slice + convertRowsToUsage inside queryInfluxDB (indexer/pkg/dz/telemetry/usage/view.go:1056, conversion at :1185, ~300 lines): 6.9M raw map[string]any rows are sorted by timestamp, then walked to compute per-key deltas, forward-fill sparse counters, and join link metadata, emitting 4.6M InterfaceUsage records.
Because a capped catch-up cycle advances the watermark by 10 minutes of data, a cycle must finish in under 10 minutes for ingest to converge. At 10m43s staging is at 0.81× real time and diverging — and the conversion phase alone (7m04s) is 71% of that 10-minute budget.
Why this is worth attacking directly
Capacity work has taken this as far as it goes cheaply. infra#2180 (indexer 2 → 4 CPU) produced near-linear gains on the other two phases — Flux read 6m29s → 2m32s, insert 2m6s → 1m07s — but left the cycle above 10 minutes because conversion dominates. infra#2194 (4 → 8 CPU) is in flight and its outcome hinges entirely on whether this phase is CPU-bound:
If conversion halves: 76s + 212s + 34s ≈ 5m22s → 1.86× real time, comfortable.
If it does not: 76s + 424s + 34s ≈ 8m54s → 1.12× real time, marginal.
Either way, halving this one phase is worth more than any remaining capacity step, and it is the difference between staging tracking real time with margin and sitting on the edge.
Suggested lines of investigation
Profile it first. A pprof CPU profile over one cycle would say whether the cost is JSON/interface decoding, the sort, map allocations, or the delta/forward-fill walk. Nothing below should be built before that.
[]map[string]any is a likely major cost. 6.9M rows as maps of boxed values means tens of millions of allocations and interface unwraps per cycle. Decoding into a typed struct (or reusing row buffers) would attack allocation and GC pressure directly. Related: indexer: use HTTP/CSV InfluxDB queries #150 proposes switching the transport to HTTP/CSV, which would change the decode path anyway — worth designing these together rather than twice.
The sort may be avoidable.sort.Slice over 6.9M rows with a comparator that re-extracts timestamps from map[string]any is expensive. If InfluxDB can return rows already ordered by time (or per-key ordering is sufficient), the sort could shrink or disappear.
Streaming instead of materializing. Conversion currently needs the whole window in memory before the insert starts. Processing in ordered chunks would overlap conversion with the Flux read and the insert, cutting wall time even if total CPU is unchanged — though note this interacts with the whole-window delta ordering that the sparse-counter forward-fill depends on, and with the baseline-cache contract (see the durable-partial-progress follow-up on indexer: telemetry-usage catch-up livelocks when a cycle exceeds the activity budget — staging watermark frozen 22.6h across 76 identical windows #740).
Prod relevance
Prod (8 CPU) is healthy at ~6-minute cycles against a 15-minute activity budget, so this is not urgent there. But it is the same code on the same row volumes, and it sets prod's recovery speed after any outage: the faster this phase, the faster prod climbs out of a catch-up backlog before the 24h horizon drops a span. It also directly affects how much headroom #747's adaptive span has to work with.
Context
Measured while running the staging capacity experiments for infra#2106 (infra#2180, infra#2194). Related: #740 (adaptive catch-up span, and its durable-partial-progress follow-up), #150 (HTTP/CSV InfluxDB transport).
Summary
66% of a staging telemetry-usage refresh cycle is spent converting Flux rows in-process — more than the InfluxDB read and the ClickHouse insert combined. This is now the dominant term in whether ingest keeps up with real time, and it is a code question rather than a capacity one.
Full breakdown of one staging cycle (2026-08-06 14:35:24 → 14:46:08, mainnet-beta, indexer at 4 CPU):
queried baselines from clickhouse(cache hit)main influxdb query completed(6,899,363 rows)queried influxdb(4,611,619 records)inserted data to clickhouserefresh completedThat middle phase is
sort.Slice+convertRowsToUsageinsidequeryInfluxDB(indexer/pkg/dz/telemetry/usage/view.go:1056, conversion at:1185, ~300 lines): 6.9M rawmap[string]anyrows are sorted by timestamp, then walked to compute per-key deltas, forward-fill sparse counters, and join link metadata, emitting 4.6MInterfaceUsagerecords.Because a capped catch-up cycle advances the watermark by 10 minutes of data, a cycle must finish in under 10 minutes for ingest to converge. At 10m43s staging is at 0.81× real time and diverging — and the conversion phase alone (7m04s) is 71% of that 10-minute budget.
Why this is worth attacking directly
Capacity work has taken this as far as it goes cheaply. infra#2180 (indexer 2 → 4 CPU) produced near-linear gains on the other two phases — Flux read 6m29s → 2m32s, insert 2m6s → 1m07s — but left the cycle above 10 minutes because conversion dominates. infra#2194 (4 → 8 CPU) is in flight and its outcome hinges entirely on whether this phase is CPU-bound:
Either way, halving this one phase is worth more than any remaining capacity step, and it is the difference between staging tracking real time with margin and sitting on the edge.
Suggested lines of investigation
pprofCPU profile over one cycle would say whether the cost is JSON/interface decoding, the sort, map allocations, or the delta/forward-fill walk. Nothing below should be built before that.[]map[string]anyis a likely major cost. 6.9M rows as maps of boxed values means tens of millions of allocations and interface unwraps per cycle. Decoding into a typed struct (or reusing row buffers) would attack allocation and GC pressure directly. Related: indexer: use HTTP/CSV InfluxDB queries #150 proposes switching the transport to HTTP/CSV, which would change the decode path anyway — worth designing these together rather than twice.sort.Sliceover 6.9M rows with a comparator that re-extracts timestamps frommap[string]anyis expensive. If InfluxDB can return rows already ordered by time (or per-key ordering is sufficient), the sort could shrink or disappear.Prod relevance
Prod (8 CPU) is healthy at ~6-minute cycles against a 15-minute activity budget, so this is not urgent there. But it is the same code on the same row volumes, and it sets prod's recovery speed after any outage: the faster this phase, the faster prod climbs out of a catch-up backlog before the 24h horizon drops a span. It also directly affects how much headroom #747's adaptive span has to work with.
Context
Measured while running the staging capacity experiments for infra#2106 (infra#2180, infra#2194). Related: #740 (adaptive catch-up span, and its durable-partial-progress follow-up), #150 (HTTP/CSV InfluxDB transport).