java-topology/defects/pyroscope/patch/pyroscope-0001-getblockstats-ulids-set.patch
russell@unturf.com 802e51430c
wave13: pyroscope-0001 UNDF-1301 (47x-438x GetBlockStats) + 5 clean-scan additions
Flagship: pyroscope PhlareDB.GetBlockStats slices.Contains per block
(O(B*U) -> O(B+U)). Long-retention tenants with 5k-10k blocks pay 1M+
membership checks per block-stats RPC. Set hoist: 438x at B=10k U=1k.

Wave 13 honor roll: lima, apollo-server, act, firecracker, nix.
Cumulative: 59 projects.
2026-04-25 14:30:18 -04:00

56 lines
2.5 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000001301
# CWE-407: Algorithmic Complexity — O(B×U) → O(B+U) in PhlareDB.GetBlockStats
#
# Defect: pkg/phlaredb/phlaredb.go GetBlockStats iterates three block sets
# (heads, flushing, queriers) and for EACH block calls
# slices.Contains(req.Msg.GetUlids(), h.meta.ULID.String()). slices.Contains
# is O(U) linear scan. Per-request cost: O(B × U) where B = total blocks
# across all three sets, U = requested ULID count.
#
# Real-world scale: pyroscope tenants storing weeks of continuous profiles
# accumulate thousands of block queriers. Operators issuing block-stats
# queries with hundreds of ULIDs pay 1M+ membership checks per call.
# ULID.String() also re-formats per iteration, multiplying allocations.
#
# Fix: Hoist req.Msg.GetUlids() into a map[string]struct{}{} once before the
# loops. Per-iter cost drops from O(U) to O(1). Total cost: O(B + U).
#
# Complexity gate (defects/pyroscope/bench/bench-pyroscope-0001.py):
# B=2000, U=200: defective ~50ms, fixed <2ms (>=25× speedup)
# k-scaling 5×: time ratio must be <17.5×
--- a/pkg/phlaredb/phlaredb.go
+++ b/pkg/phlaredb/phlaredb.go
@@ -594,16 +594,21 @@ func (f *PhlareDB) GetBlockStats(ctx context.Context, req *connect.Request[inges
defer sp.Finish()
res := &ingestv1.GetBlockStatsResponse{}
+ // Hoist requested ULIDs into a set so per-block membership is O(1) instead of
+ // O(U) slices.Contains. GetBlockStats walks heads + flushing + queriers
+ // (potentially thousands of blocks); the linear scan is O(B*U) per request.
+ requested := make(map[string]struct{}, len(req.Msg.GetUlids()))
+ for _, u := range req.Msg.GetUlids() {
+ requested[u] = struct{}{}
+ }
f.headLock.RLock()
for _, h := range f.heads {
- if slices.Contains(req.Msg.GetUlids(), h.meta.ULID.String()) {
+ if _, ok := requested[h.meta.ULID.String()]; ok {
res.BlockStats = append(res.BlockStats, h.GetMetaStats().ConvertToBlockStats())
}
}
for _, h := range f.flushing {
- if slices.Contains(req.Msg.GetUlids(), h.meta.ULID.String()) {
+ if _, ok := requested[h.meta.ULID.String()]; ok {
res.BlockStats = append(res.BlockStats, h.GetMetaStats().ConvertToBlockStats())
}
}
f.headLock.RUnlock()
f.blockQuerier.queriersLock.RLock()
for _, q := range f.blockQuerier.queriers {
- if slices.Contains(req.Msg.GetUlids(), q.meta.ULID.String()) {
+ if _, ok := requested[q.meta.ULID.String()]; ok {
res.BlockStats = append(res.BlockStats, q.GetMetaStats().ConvertToBlockStats())
}
}
f.blockQuerier.queriersLock.RUnlock()