github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/functions/linear/histogram_quantile.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package linear 22 23 import ( 24 "fmt" 25 "math" 26 "sort" 27 "strconv" 28 29 "github.com/m3db/m3/src/query/block" 30 "github.com/m3db/m3/src/query/executor/transform" 31 "github.com/m3db/m3/src/query/functions/utils" 32 "github.com/m3db/m3/src/query/models" 33 "github.com/m3db/m3/src/query/parser" 34 "github.com/m3db/m3/src/query/util" 35 ) 36 37 const ( 38 // HistogramQuantileType calculates the quantile for histogram buckets. 39 // 40 // NB: each sample must contain a tag with a bucket name (given by tag 41 // options) that denotes the upper bound of that bucket; series without this 42 // tag are ignored. 43 HistogramQuantileType = "histogram_quantile" 44 initIndexBucketLength = 10 45 ) 46 47 // NewHistogramQuantileOp creates a new histogram quantile operation. 48 func NewHistogramQuantileOp( 49 args []interface{}, 50 opType string, 51 ) (parser.Params, error) { 52 if len(args) != 1 { 53 return nil, fmt.Errorf( 54 "invalid number of args for histogram_quantile: %d", len(args)) 55 } 56 57 if opType != HistogramQuantileType { 58 return nil, fmt.Errorf("operator not supported: %s", opType) 59 } 60 61 q, ok := args[0].(float64) 62 if !ok { 63 return nil, fmt.Errorf("unable to cast to scalar argument: %v", args[0]) 64 } 65 66 return newHistogramQuantileOp(q, opType), nil 67 } 68 69 // histogramQuantileOp stores required properties for histogram quantile ops. 70 type histogramQuantileOp struct { 71 q float64 72 opType string 73 } 74 75 // OpType for the operator. 76 func (o histogramQuantileOp) OpType() string { 77 return o.opType 78 } 79 80 // String representation. 81 func (o histogramQuantileOp) String() string { 82 return fmt.Sprintf("type: %s", o.OpType()) 83 } 84 85 // Node creates an execution node. 86 func (o histogramQuantileOp) Node( 87 controller *transform.Controller, 88 _ transform.Options, 89 ) transform.OpNode { 90 return &histogramQuantileNode{ 91 op: o, 92 controller: controller, 93 } 94 } 95 96 func newHistogramQuantileOp( 97 q float64, 98 opType string, 99 ) histogramQuantileOp { 100 return histogramQuantileOp{ 101 q: q, 102 opType: opType, 103 } 104 } 105 106 type histogramQuantileNode struct { 107 op histogramQuantileOp 108 controller *transform.Controller 109 } 110 111 type bucketValue struct { 112 upperBound float64 113 value float64 114 } 115 116 type indexedBucket struct { 117 upperBound float64 118 idx int 119 } 120 121 type indexedBuckets struct { 122 buckets []indexedBucket 123 tags models.Tags 124 } 125 126 func (b indexedBuckets) Len() int { return len(b.buckets) } 127 func (b indexedBuckets) Swap(i, j int) { 128 b.buckets[i], b.buckets[j] = b.buckets[j], b.buckets[i] 129 } 130 func (b indexedBuckets) Less(i, j int) bool { 131 return b.buckets[i].upperBound < b.buckets[j].upperBound 132 } 133 134 type bucketedSeries map[string]indexedBuckets 135 136 type validSeriesBuckets []indexedBuckets 137 138 func (b validSeriesBuckets) Len() int { return len(b) } 139 func (b validSeriesBuckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 140 func (b validSeriesBuckets) Less(i, j int) bool { 141 if len(b[i].buckets) == 0 { 142 return false 143 } 144 145 if len(b[j].buckets) == 0 { 146 return true 147 } 148 149 // An arbitrarily chosen sort that guarantees deterministic results. 150 return b[i].buckets[0].idx < b[j].buckets[0].idx 151 } 152 153 func gatherSeriesToBuckets(metas []block.SeriesMeta) validSeriesBuckets { 154 bucketsForID := make(bucketedSeries, initIndexBucketLength) 155 for i, meta := range metas { 156 tags := meta.Tags 157 value, found := tags.Bucket() 158 if !found { 159 // this series does not have a bucket tag; drop it from the output. 160 continue 161 } 162 163 bound, err := strconv.ParseFloat(string(value), 64) 164 if err != nil { 165 // invalid bounds value for the bucket; drop it from the output. 166 continue 167 } 168 169 excludeTags := [][]byte{tags.Opts.MetricName(), tags.Opts.BucketName()} 170 tagsWithoutKeys := tags.TagsWithoutKeys(excludeTags) 171 id := string(tagsWithoutKeys.ID()) 172 newBucket := indexedBucket{ 173 upperBound: bound, 174 idx: i, 175 } 176 177 if buckets, found := bucketsForID[id]; !found { 178 // add a single indexed bucket for this ID with the current index only. 179 newBuckets := make([]indexedBucket, 0, initIndexBucketLength) 180 newBuckets = append(newBuckets, newBucket) 181 bucketsForID[id] = indexedBuckets{ 182 buckets: newBuckets, 183 tags: tagsWithoutKeys, 184 } 185 } else { 186 buckets.buckets = append(buckets.buckets, newBucket) 187 bucketsForID[id] = buckets 188 } 189 } 190 191 return sanitizeBuckets(bucketsForID) 192 } 193 194 // sanitize sorts the bucket maps by upper bound, dropping any series which 195 // have less than two buckets, or any that do not have an upper bound of +Inf 196 func sanitizeBuckets(bucketMap bucketedSeries) validSeriesBuckets { 197 validSeriesBuckets := make(validSeriesBuckets, 0, len(bucketMap)) 198 for _, buckets := range bucketMap { 199 if len(buckets.buckets) < 2 { 200 continue 201 } 202 203 sort.Sort(buckets) 204 maxBound := buckets.buckets[len(buckets.buckets)-1].upperBound 205 if !math.IsInf(maxBound, 1) { 206 continue 207 } 208 209 validSeriesBuckets = append(validSeriesBuckets, buckets) 210 } 211 212 sort.Sort(validSeriesBuckets) 213 return validSeriesBuckets 214 } 215 216 func bucketQuantile(q float64, buckets []bucketValue) float64 { 217 // NB: some valid buckets may have been purged if the values at the current 218 // step for that series are not present. 219 if len(buckets) < 2 { 220 return math.NaN() 221 } 222 223 // NB: similar situation here if the max bound bucket does not have a value 224 // at this point, it is necessary to re-check. 225 if !math.IsInf(buckets[len(buckets)-1].upperBound, 1) { 226 return math.NaN() 227 } 228 229 rank := q * buckets[len(buckets)-1].value 230 231 bucketIndex := sort.Search(len(buckets)-1, func(i int) bool { 232 return buckets[i].value >= rank 233 }) 234 235 if bucketIndex == len(buckets)-1 { 236 return buckets[len(buckets)-2].upperBound 237 } 238 239 if bucketIndex == 0 && buckets[0].upperBound <= 0 { 240 return buckets[0].upperBound 241 } 242 243 var ( 244 bucketStart float64 245 bucketEnd = buckets[bucketIndex].upperBound 246 count = buckets[bucketIndex].value 247 ) 248 249 if bucketIndex > 0 { 250 bucketStart = buckets[bucketIndex-1].upperBound 251 count -= buckets[bucketIndex-1].value 252 rank -= buckets[bucketIndex-1].value 253 } 254 255 return bucketStart + (bucketEnd-bucketStart)*rank/count 256 } 257 258 func (n *histogramQuantileNode) Params() parser.Params { 259 return n.op 260 } 261 262 // Process the block 263 func (n *histogramQuantileNode) Process( 264 queryCtx *models.QueryContext, 265 ID parser.NodeID, 266 b block.Block, 267 ) error { 268 return transform.ProcessSimpleBlock(n, n.controller, queryCtx, ID, b) 269 } 270 271 func (n *histogramQuantileNode) ProcessBlock( 272 queryCtx *models.QueryContext, 273 ID parser.NodeID, 274 b block.Block, 275 ) (block.Block, error) { 276 stepIter, err := b.StepIter() 277 if err != nil { 278 return nil, err 279 } 280 281 meta := b.Meta() 282 seriesMetas := utils.FlattenMetadata(meta, stepIter.SeriesMeta()) 283 seriesBuckets := gatherSeriesToBuckets(seriesMetas) 284 285 q := n.op.q 286 if q < 0 || q > 1 { 287 return processInvalidQuantile(queryCtx, q, seriesBuckets, meta, stepIter, n.controller) 288 } 289 290 return processValidQuantile(queryCtx, q, seriesBuckets, meta, stepIter, n.controller) 291 } 292 293 func setupBuilder( 294 queryCtx *models.QueryContext, 295 seriesBuckets validSeriesBuckets, 296 meta block.Metadata, 297 stepIter block.StepIter, 298 controller *transform.Controller, 299 ) (block.Builder, error) { 300 metas := make([]block.SeriesMeta, 0, len(seriesBuckets)) 301 for _, v := range seriesBuckets { 302 metas = append(metas, block.SeriesMeta{ 303 Tags: v.tags, 304 }) 305 } 306 307 builder, err := controller.BlockBuilder(queryCtx, meta, metas) 308 if err != nil { 309 return nil, err 310 } 311 312 if err = builder.AddCols(stepIter.StepCount()); err != nil { 313 return nil, err 314 } 315 316 return builder, nil 317 } 318 319 // Enforce monotonicity for binary search to work. 320 // See https://github.com/prometheus/prometheus/commit/896f951e6846ce252d9d19fd4707a4110ceda5ee 321 func ensureMonotonic(bucketValues []bucketValue) { 322 max := math.Inf(-1) 323 for i := range bucketValues { 324 switch { 325 case bucketValues[i].value >= max: 326 max = bucketValues[i].value 327 case bucketValues[i].value < max: 328 bucketValues[i].value = max 329 } 330 } 331 } 332 333 func processValidQuantile( 334 queryCtx *models.QueryContext, 335 q float64, 336 seriesBuckets validSeriesBuckets, 337 meta block.Metadata, 338 stepIter block.StepIter, 339 controller *transform.Controller, 340 ) (block.Block, error) { 341 builder, err := setupBuilder(queryCtx, seriesBuckets, meta, stepIter, controller) 342 if err != nil { 343 return nil, err 344 } 345 346 for index := 0; stepIter.Next(); index++ { 347 step := stepIter.Current() 348 values := step.Values() 349 bucketValues := make([]bucketValue, 0, initIndexBucketLength) 350 351 aggregatedValues := make([]float64, 0, len(seriesBuckets)) 352 for _, b := range seriesBuckets { 353 buckets := b.buckets 354 // clear previous bucket values. 355 bucketValues = bucketValues[:0] 356 for _, bucket := range buckets { 357 // Only add non-NaN values to contention for the calculation. 358 val := values[bucket.idx] 359 if !math.IsNaN(val) { 360 bucketValues = append( 361 bucketValues, bucketValue{ 362 upperBound: bucket.upperBound, 363 value: val, 364 }, 365 ) 366 } 367 } 368 369 ensureMonotonic(bucketValues) 370 371 aggregatedValues = append(aggregatedValues, bucketQuantile(q, bucketValues)) 372 } 373 374 if err := builder.AppendValues(index, aggregatedValues); err != nil { 375 return nil, err 376 } 377 } 378 379 if err = stepIter.Err(); err != nil { 380 return nil, err 381 } 382 383 return builder.Build(), nil 384 } 385 386 func processInvalidQuantile( 387 queryCtx *models.QueryContext, 388 q float64, 389 seriesBuckets validSeriesBuckets, 390 meta block.Metadata, 391 stepIter block.StepIter, 392 controller *transform.Controller, 393 ) (block.Block, error) { 394 builder, err := setupBuilder(queryCtx, seriesBuckets, meta, stepIter, controller) 395 if err != nil { 396 return nil, err 397 } 398 399 // Set the values to an infinity of the appropriate sign; anything less than 0 400 // becomes -Inf, anything greather than one becomes +Inf. 401 sign := 1 402 if q < 0 { 403 sign = -1 404 } 405 406 setValue := math.Inf(sign) 407 outValues := make([]float64, len(seriesBuckets)) 408 util.Memset(outValues, setValue) 409 for index := 0; stepIter.Next(); index++ { 410 if err := builder.AppendValues(index, outValues); err != nil { 411 return nil, err 412 } 413 } 414 415 if err = stepIter.Err(); err != nil { 416 return nil, err 417 } 418 419 return builder.Build(), nil 420 }