github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/graphite/native/functions.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 native 22 23 import ( 24 "bytes" 25 "errors" 26 "fmt" 27 "reflect" 28 "runtime" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/m3db/m3/src/query/block" 34 "github.com/m3db/m3/src/query/graphite/common" 35 "github.com/m3db/m3/src/query/graphite/ts" 36 xerrors "github.com/m3db/m3/src/x/errors" 37 ) 38 39 var ( 40 funcMut sync.RWMutex 41 functions = map[string]*Function{} 42 ) 43 44 // list of graphite function name strings. (not whole list, update on-demand) 45 const ( 46 averageFnName = "average" 47 averageSeriesFnName = "averageSeries" 48 avgFnName = "avg" 49 countFnName = "count" 50 countSeriesFnName = "countSeries" 51 currentFnName = "current" 52 diffFnName = "diff" 53 diffSeriesFnName = "diffSeries" 54 emptyFnName = "" 55 lastFnName = "last" 56 lastSeriesFnName = "lastSeries" 57 maxFnName = "max" 58 maxSeriesFnName = "maxSeries" 59 medianFnName = "median" 60 medianSeriesFnName = "medianSeries" 61 minFnName = "min" 62 minSeriesFnName = "minSeries" 63 multiplyFnName = "multiply" 64 multiplySeriesFnName = "multiplySeries" 65 powSeriesFnName = "powSeries" 66 rangeFnName = "range" 67 rangeOfFnName = "rangeOf" 68 rangeOfSeriesFnName = "rangeOfSeries" 69 stdevFnName = "stdev" 70 stddevFnName = "stddev" 71 stddevSeriesFnName = "stddevSeries" 72 sumFnName = "sum" 73 sumSeriesFnName = "sumSeries" 74 totalFnName = "total" 75 ) 76 77 // registerFunction is used to register a function under a specific name 78 func registerFunction(f interface{}) (*Function, error) { 79 fn, err := buildFunction(f) 80 if err != nil { 81 return nil, err 82 } 83 84 funcMut.Lock() 85 defer funcMut.Unlock() 86 87 if functions[fn.name] != nil { 88 return nil, fmt.Errorf("func %s already registered", fn.name) 89 } 90 functions[fn.name] = fn 91 return fn, nil 92 } 93 94 // MustRegisterFunction registers a function, issuing a panic if the function cannot be registered 95 func MustRegisterFunction(f interface{}) *Function { 96 if fn, err := registerFunction(f); err != nil { 97 if name, nerr := functionName(f); nerr == nil { 98 err = fmt.Errorf("could not register %s: %v", name, err) 99 } 100 panic(err) 101 } else { 102 return fn 103 } 104 } 105 106 // registerAliasedFunction is used to register a function under an alias 107 func registerAliasedFunction(alias string, f interface{}) error { 108 fname, err := functionName(f) 109 if err != nil { 110 return err 111 } 112 113 funcMut.Lock() 114 defer funcMut.Unlock() 115 116 if functions[alias] != nil { 117 return fmt.Errorf("func %s already registered", alias) 118 } 119 120 fn := functions[fname] 121 if fn == nil { 122 return fmt.Errorf("target function %s not registered", fname) 123 } 124 125 functions[alias] = fn 126 return nil 127 } 128 129 // MustRegisterAliasedFunction registers a function under an alias, issuing a panic if the function 130 // cannot be registered 131 func MustRegisterAliasedFunction(fname string, f interface{}) { 132 if err := registerAliasedFunction(fname, f); err != nil { 133 panic(err) 134 } 135 } 136 137 // findFunction finds a function with the given name 138 func findFunction(name string) *Function { 139 funcMut.RLock() 140 defer funcMut.RUnlock() 141 142 return functions[name] 143 } 144 145 // reflectTypeSet is a set of reflect.Type objects 146 type reflectTypeSet []reflect.Type 147 148 // contains checks whether the type set contains the given type 149 func (ts reflectTypeSet) contains(reflectType reflect.Type) bool { 150 for i := range ts { 151 if ts[i] == reflectType { 152 return true 153 } 154 } 155 156 return false 157 } 158 159 // singlePathSpec represents one wildcard pathspec argument that may fetch multiple time series 160 type singlePathSpec ts.SeriesList 161 162 // multiplePathSpecs represents a variadic number of wildcard pathspecs 163 type multiplePathSpecs ts.SeriesList 164 165 // genericInterface represents a value with an arbitrary type 166 type genericInterface interface{} 167 168 // contextShiftFunc generates a shifted context based on an input context 169 type contextShiftFunc func(*common.Context) *common.Context 170 171 // unaryTransformer takes in one series and returns a transformed series. 172 type unaryTransformer func(ts.SeriesList) (ts.SeriesList, error) 173 174 // contextShiftAdjustFunc determines after an initial context shift whether 175 // an adjustment is necessary or not 176 type contextShiftAdjustFunc func( 177 shiftedContext *common.Context, 178 bootstrappedSeries ts.SeriesList, 179 ) (*common.Context, bool, error) 180 181 // unaryContextShifter contains a contextShiftFunc for generating shift contexts 182 // as well as a unaryTransformer for transforming one series to another. 183 type unaryContextShifter struct { 184 ContextShiftFunc contextShiftFunc 185 UnaryTransformer unaryTransformer 186 ContextShiftAdjustFunc contextShiftAdjustFunc 187 } 188 189 var ( 190 contextPtrType = reflect.TypeOf(&common.Context{}) 191 timeSeriesType = reflect.TypeOf(&ts.Series{}) 192 timeSeriesListType = reflect.SliceOf(timeSeriesType) 193 seriesListType = reflect.TypeOf(ts.NewSeriesList()) 194 unaryContextShifterPtrType = reflect.TypeOf(&unaryContextShifter{}) 195 singlePathSpecType = reflect.TypeOf(singlePathSpec{}) 196 multiplePathSpecsType = reflect.TypeOf(multiplePathSpecs{}) 197 interfaceType = reflect.TypeOf([]genericInterface{}).Elem() 198 float64Type = reflect.TypeOf(float64(100)) 199 float64SliceType = reflect.SliceOf(float64Type) 200 intType = reflect.TypeOf(int(0)) 201 intSliceType = reflect.SliceOf(intType) 202 stringType = reflect.TypeOf("") 203 stringSliceType = reflect.SliceOf(stringType) 204 boolType = reflect.TypeOf(false) 205 boolSliceType = reflect.SliceOf(boolType) 206 errorType = reflect.TypeOf((*error)(nil)).Elem() 207 genericInterfaceType = reflect.TypeOf((*genericInterface)(nil)).Elem() 208 ) 209 210 var allowableTypes = reflectTypeSet{ 211 // these are for return types 212 timeSeriesListType, 213 unaryContextShifterPtrType, 214 seriesListType, 215 singlePathSpecType, 216 multiplePathSpecsType, 217 interfaceType, // only for function parameters 218 float64Type, 219 float64SliceType, 220 intType, 221 intSliceType, 222 stringType, 223 stringSliceType, 224 boolType, 225 boolSliceType, 226 } 227 228 var ( 229 errNonFunction = xerrors.NewInvalidParamsError(errors.New("not a function")) 230 errNeedsArgument = xerrors.NewInvalidParamsError(errors.New("functions must take at least 1 argument")) 231 errNoContext = xerrors.NewInvalidParamsError(errors.New("first argument must be a context")) 232 errInvalidReturn = xerrors.NewInvalidParamsError(errors.New("functions must return a value and an error")) 233 ) 234 235 // Function contains a function to invoke along with metadata about 236 // the function's argument and return type. 237 type Function struct { 238 name string 239 f reflect.Value 240 in []reflect.Type 241 defaults map[uint8]interface{} 242 out reflect.Type 243 variadic bool 244 245 disableUnaryContextShiftFetchOptimization bool 246 } 247 248 // WithDefaultParams provides default parameters for functions 249 func (f *Function) WithDefaultParams(defaultParams map[uint8]interface{}) *Function { 250 for index := range defaultParams { 251 if int(index) <= 0 || int(index) > len(f.in) { 252 panic(fmt.Sprintf("Default parameter #%d is out-of-range", index)) 253 } 254 } 255 f.defaults = defaultParams 256 return f 257 } 258 259 // WithoutUnaryContextShifterSkipFetchOptimization allows a function to skip 260 // the optimization that avoids fetching data for the first execution phase 261 // of a unary context shifted function (where it is called the first time 262 // without any series populated as input to the function and relies on 263 // execution of the unary context shifter with the shifted series solely). 264 // Note: This is useful for the "movingX" family of functions that require 265 // that require actually seeing what the step size is sometimes of the series 266 // from the first phase of execution to determine how much to look back for the 267 // context shift phase. 268 func (f *Function) WithoutUnaryContextShifterSkipFetchOptimization() *Function { 269 if f.out != unaryContextShifterPtrType { 270 panic("Skip fetch optimization only available to unary context shifters") 271 } 272 f.disableUnaryContextShiftFetchOptimization = true 273 return f 274 } 275 276 func functionName(f interface{}) (string, error) { 277 v := reflect.ValueOf(f) 278 t := v.Type() 279 if t.Kind() != reflect.Func { 280 return "", errNonFunction 281 } 282 283 nameParts := strings.Split(runtime.FuncForPC(v.Pointer()).Name(), ".") 284 return nameParts[len(nameParts)-1], nil 285 } 286 287 // validateContextShiftingFn validates if a function is a context shifting function. 288 func validateContextShiftingFn(in []reflect.Type) { 289 // check that we have exactly *one* singlePathSpec parameter 290 singlePathSpecParams := 0 291 singlePathSpecIndex := -1 292 for i, param := range in { 293 if param == singlePathSpecType { 294 singlePathSpecParams++ 295 singlePathSpecIndex = i 296 } 297 } 298 if singlePathSpecParams != 1 { 299 panic("A context-shifting function must have exactly one singlePathSpec parameter") 300 } 301 if singlePathSpecIndex != 0 { 302 panic("A context-shifting function must have the singlePathSpec parameter as its first parameter") 303 } 304 } 305 306 // buildFunction takes a reflection reference to a function and returns 307 // the function metadata 308 func buildFunction(f interface{}) (*Function, error) { 309 fname, err := functionName(f) 310 if err != nil { 311 return nil, err 312 } 313 v := reflect.ValueOf(f) 314 t := v.Type() 315 if t.NumIn() == 0 { 316 return nil, errNeedsArgument 317 } 318 319 if ctx := t.In(0); ctx != contextPtrType { 320 return nil, errNoContext 321 } 322 323 var lastType reflect.Type 324 in := make([]reflect.Type, 0, t.NumIn()-1) 325 for i := 1; i < t.NumIn(); i++ { 326 inArg := t.In(i) 327 if !(allowableTypes.contains(inArg)) { 328 return nil, fmt.Errorf("invalid arg %d: %s is not supported", i, inArg.Name()) 329 } 330 if inArg == multiplePathSpecsType && i != t.NumIn()-1 { 331 return nil, fmt.Errorf("invalid arg %d: multiplePathSpecs must be the last arg", i) 332 } 333 334 lastType = inArg 335 in = append(in, inArg) 336 } 337 338 variadic := lastType == multiplePathSpecsType || 339 (lastType != nil && 340 lastType.Kind() == reflect.Slice && 341 lastType != singlePathSpecType) 342 343 if variadic { // remove slice-ness of the variadic arg 344 if lastType != multiplePathSpecsType { 345 in[len(in)-1] = in[len(in)-1].Elem() 346 } 347 } 348 349 if t.NumOut() != 2 { 350 return nil, errInvalidReturn 351 } 352 353 out := t.Out(0) 354 if !allowableTypes.contains(out) { 355 return nil, fmt.Errorf("invalid return type %s", out.Name()) 356 } else if out == unaryContextShifterPtrType { 357 validateContextShiftingFn(in) 358 } 359 360 if ret2 := t.Out(1); ret2 != errorType { 361 return nil, errInvalidReturn 362 } 363 364 return &Function{ 365 name: fname, 366 f: v, 367 in: in, 368 out: out, 369 variadic: variadic, 370 }, nil 371 } 372 373 // call calls the function with non-reflected values 374 func (f *Function) call(ctx *common.Context, args []interface{}) (interface{}, error) { 375 values := make([]reflect.Value, len(args)) 376 for i := range args { 377 values[i] = reflect.ValueOf(args[i]) 378 } 379 380 out, err := f.reflectCall(ctx, values) 381 if err != nil { 382 return nil, err 383 } 384 385 return out.Interface(), err 386 } 387 388 // reflectCall calls the function with reflected values, passing in the provided context and parameters 389 func (f *Function) reflectCall(ctx *common.Context, args []reflect.Value) (reflect.Value, error) { 390 var instats []common.TraceStats 391 392 in := make([]reflect.Value, 0, len(args)+1) 393 in = append(in, reflect.ValueOf(ctx)) 394 for _, arg := range args { 395 in = append(in, arg) 396 if isTimeSeries(arg) { 397 instats = append(instats, getStats(arg)) 398 } 399 } 400 401 // special case handling of multiplePathSpecs 402 // NB(r): This code sucks, and it would be better if we just removed 403 // multiplePathSpecs altogether and have the functions use real variadic 404 // ts.SeriesList arguments so we don't have to autocollapse when calling here. 405 // Notably singlePathSpec should also go and just replace usages with 406 // barebones ts.SeriesList. Then we can get rid of this code below and 407 // the code the casts ts.SeriesList to the correct typealias of ts.SeriesList. 408 if len(in) > len(f.in)+1 && len(f.in) > 0 && f.in[len(f.in)-1] == multiplePathSpecsType { 409 var ( 410 series = make([]*ts.Series, 0, len(in)) 411 // Assume all sorted until proven otherwise 412 sortedAll = true 413 meta = block.NewResultMetadata() 414 ) 415 for i := len(f.in); i < len(in); i++ { 416 v := in[i].Interface().(ts.SeriesList) 417 418 // If any series lists are not sorted then the result 419 // is not in deterministic sort order 420 if sortedAll && !v.SortApplied { 421 sortedAll = false 422 } 423 424 meta = meta.CombineMetadata(v.Metadata) 425 series = append(series, v.Values...) 426 } 427 428 in[len(f.in)] = reflect.ValueOf(ts.SeriesList{ 429 Values: series, 430 // Only consider the aggregation of all these series lists 431 // sorted if and only if all originally had a sort applied 432 SortApplied: sortedAll, 433 Metadata: meta, 434 }) 435 436 in = in[:len(f.in)+1] 437 } 438 439 numTypes := len(f.in) 440 numRequiredTypes := numTypes 441 if f.variadic { 442 // Variadic can avoid specifying the last arg. 443 numRequiredTypes-- 444 } 445 if len(in) < numRequiredTypes { 446 err := fmt.Errorf("call args mismatch: expected at least %d, actual %d", 447 len(f.in), len(in)) 448 return reflect.Value{}, err 449 } 450 451 // Cast to the expected typealias type of ts.SeriesList before calling 452 for i, arg := range in { 453 if !arg.IsValid() { 454 // Zero value arg is a nil. 455 in[i] = reflect.New(genericInterfaceType).Elem() 456 continue 457 } 458 typeArg := arg.Type() 459 if typeArg != seriesListType { 460 continue 461 } 462 // NB(r): Poor form, ctx is not in f.in for no reason it seems... 463 typeIdx := i - 1 464 if i >= numTypes { 465 typeIdx = numTypes - 1 466 } 467 l := arg.Interface().(ts.SeriesList) 468 switch f.in[typeIdx] { 469 case singlePathSpecType, genericInterfaceType: 470 in[i] = reflect.ValueOf(singlePathSpec(l)) 471 case multiplePathSpecsType: 472 in[i] = reflect.ValueOf(multiplePathSpecs(l)) 473 default: 474 err := fmt.Errorf("cannot cast series to unexpected type: %s", 475 f.in[typeIdx].String()) 476 return reflect.Value{}, err 477 } 478 } 479 480 beginCall := time.Now() 481 out := f.f.Call(in) 482 outVal, errVal := out[0], out[1] 483 var err error 484 if !errVal.IsNil() { 485 err = errVal.Interface().(error) 486 return outVal, err 487 } 488 489 if ctx.TracingEnabled() { 490 var outstats common.TraceStats 491 if isTimeSeries(outVal) { 492 outstats = getStats(outVal) 493 } 494 495 ctx.Trace(common.Trace{ 496 ActivityName: f.name, 497 Inputs: instats, 498 Outputs: outstats, 499 Duration: time.Since(beginCall), 500 }) 501 } 502 503 return outVal, nil 504 } 505 506 // A funcArg is an argument to a function that gets resolved at runtime 507 type funcArg interface { 508 ASTNode 509 Evaluate(ctx *common.Context) (reflect.Value, error) 510 Type() reflect.Type 511 CompatibleWith(reflectType reflect.Type) bool 512 } 513 514 // A constFuncArg is a function argument that is a constant value 515 type constFuncArg struct { 516 value reflect.Value 517 } 518 519 func newConstArg(i interface{}) funcArg { return constFuncArg{value: reflect.ValueOf(i)} } 520 func newBoolConst(b bool) funcArg { return constFuncArg{value: reflect.ValueOf(b)} } 521 func newStringConst(s string) funcArg { return constFuncArg{value: reflect.ValueOf(s)} } 522 func newFloat64Const(n float64) funcArg { return constFuncArg{value: reflect.ValueOf(n)} } 523 func newIntConst(n int) funcArg { return constFuncArg{value: reflect.ValueOf(n)} } 524 525 func (c constFuncArg) Evaluate(ctx *common.Context) (reflect.Value, error) { return c.value, nil } 526 func (c constFuncArg) Type() reflect.Type { return c.value.Type() } 527 func (c constFuncArg) CompatibleWith(reflectType reflect.Type) bool { 528 return c.value.Type() == reflectType || reflectType == interfaceType 529 } 530 func (c constFuncArg) String() string { return fmt.Sprintf("%v", c.value.Interface()) } 531 func (c constFuncArg) PathExpression() (string, bool) { return "", false } 532 func (c constFuncArg) CallExpression() (CallASTNode, bool) { return nil, false } 533 534 // A functionCall is an actual call to a function, with resolution for arguments 535 type functionCall struct { 536 f *Function 537 in []funcArg 538 } 539 540 func (call *functionCall) Name() string { 541 return call.f.name 542 } 543 544 func (call *functionCall) Arguments() []ASTNode { 545 args := make([]ASTNode, 0, len(call.in)) 546 for _, arg := range call.in { 547 args = append(args, arg) 548 } 549 return args 550 } 551 552 func (call *functionCall) PathExpression() (string, bool) { 553 return "", false 554 } 555 556 func (call *functionCall) CallExpression() (CallASTNode, bool) { 557 return call, true 558 } 559 560 // Evaluate evaluates the function call and returns the result as a reflect.Value 561 func (call *functionCall) Evaluate(ctx *common.Context) (reflect.Value, error) { 562 values := make([]reflect.Value, len(call.in)) 563 for i, param := range call.in { 564 // Optimization to skip fetching series for a unary context shift 565 // operation since they'll refetch after shifting. 566 // Note: You can call WithoutUnaryContextShifterSkipFetchOptimization() 567 // after registering a function if you need a unary context shift 568 // operation to have series for the initial time window fetched. 569 if call.f.out == unaryContextShifterPtrType && 570 !call.f.disableUnaryContextShiftFetchOptimization && 571 (call.f.in[i] == singlePathSpecType || call.f.in[i] == multiplePathSpecsType) { 572 values[i] = reflect.ValueOf(singlePathSpec{}) // fake parameter 573 continue 574 } 575 value, err := param.Evaluate(ctx) 576 if err != nil { 577 return reflect.Value{}, err 578 } 579 values[i] = value 580 } 581 582 result, err := call.f.reflectCall(ctx, values) 583 // if we have errors, or if we succeed and this is not a context-shifting function, 584 // we return immediately 585 if err != nil || call.f.out == seriesListType { 586 return result, err 587 } 588 589 // context shifter ptr is nil, nothing to do here, return empty series. 590 if result.IsNil() { 591 return reflect.ValueOf(ts.NewSeriesList()), nil 592 } 593 594 contextShifter := result.Elem() 595 ctxShiftingFn := contextShifter.Field(0) 596 reflected := ctxShiftingFn.Call([]reflect.Value{reflect.ValueOf(ctx)}) 597 shiftedCtx := reflected[0].Interface().(*common.Context) 598 shiftedSeries, err := call.in[0].Evaluate(shiftedCtx) 599 if err != nil { 600 return reflect.Value{}, err 601 } 602 603 // Determine if need to adjust the shift based on fetched series 604 // from the context shift. 605 MaybeAdjustShiftLoop: 606 for { 607 adjustFn := contextShifter.Field(2) 608 if adjustFn.IsNil() { 609 break MaybeAdjustShiftLoop 610 } 611 612 reflected := adjustFn.Call([]reflect.Value{ 613 reflect.ValueOf(shiftedCtx), 614 shiftedSeries, 615 }) 616 if reflectedErr := reflected[2]; !reflectedErr.IsNil() { 617 return reflect.Value{}, reflectedErr.Interface().(error) 618 } 619 620 if reflectedAdjust := reflected[1]; !reflectedAdjust.Bool() { 621 // No further adjust. 622 break MaybeAdjustShiftLoop 623 } 624 625 // Adjusted again, need to re-bootstrap from the shifted series. 626 adjustedShiftedCtx := reflected[0].Interface().(*common.Context) 627 adjustedShiftedSeries, err := call.in[0].Evaluate(adjustedShiftedCtx) 628 if err != nil { 629 return reflect.Value{}, err 630 } 631 632 // Override previously shifted context and series fetched and re-eval. 633 shiftedCtx = adjustedShiftedCtx 634 shiftedSeries = adjustedShiftedSeries 635 } 636 637 // Execute the unary transformer function with the shifted series. 638 var ( 639 transformerFn = contextShifter.Field(1) 640 ret []reflect.Value 641 ) 642 switch call.f.out { 643 case unaryContextShifterPtrType: 644 // unary function 645 ret = transformerFn.Call([]reflect.Value{shiftedSeries}) 646 default: 647 return reflect.Value{}, fmt.Errorf("unknown context shift: %v", call.f.out) 648 } 649 if !ret[1].IsNil() { 650 err = ret[1].Interface().(error) 651 } 652 return ret[0], err 653 } 654 655 func (call *functionCall) Type() reflect.Type { 656 return reflect.ValueOf(call).Type() 657 } 658 659 // CompatibleWith checks whether the function call's return is compatible with the given reflection type 660 func (call *functionCall) CompatibleWith(reflectType reflect.Type) bool { 661 if reflectType == interfaceType { 662 return true 663 } 664 if call.f.out == unaryContextShifterPtrType { 665 return reflectType == singlePathSpecType || reflectType == multiplePathSpecsType 666 } 667 return call.f.out.Kind() == reflectType.Kind() 668 } 669 670 func (call *functionCall) String() string { 671 var buf bytes.Buffer 672 buf.WriteString(call.f.name) 673 buf.WriteByte('(') 674 for i := range call.in { 675 if i > 0 { 676 buf.WriteByte(',') 677 } 678 buf.WriteString(call.in[i].String()) 679 } 680 681 buf.WriteByte(')') 682 return buf.String() 683 } 684 685 // isTimeSeries checks whether the given value contains a timeseries or 686 // timeseries list 687 func isTimeSeries(v reflect.Value) bool { 688 return v.IsValid() && v.Type() == seriesListType 689 } 690 691 // getStats gets trace stats for the given timeseries argument 692 func getStats(v reflect.Value) common.TraceStats { 693 if v.IsValid() && v.Type() == timeSeriesType { 694 return common.TraceStats{NumSeries: 1} 695 } 696 697 l := v.Interface().(ts.SeriesList) 698 return common.TraceStats{NumSeries: l.Len()} 699 }