# 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()