Home | History | Annotate | Download | only in measurement
      1 // Copyright 2014 Google Inc. All Rights Reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 // Package measurement export utility functions to manipulate/format performance profile sample values.
     16 package measurement
     17 
     18 import (
     19 	"fmt"
     20 	"strings"
     21 	"time"
     22 
     23 	"github.com/google/pprof/profile"
     24 )
     25 
     26 // ScaleProfiles updates the units in a set of profiles to make them
     27 // compatible. It scales the profiles to the smallest unit to preserve
     28 // data.
     29 func ScaleProfiles(profiles []*profile.Profile) error {
     30 	if len(profiles) == 0 {
     31 		return nil
     32 	}
     33 	periodTypes := make([]*profile.ValueType, 0, len(profiles))
     34 	for _, p := range profiles {
     35 		if p.PeriodType != nil {
     36 			periodTypes = append(periodTypes, p.PeriodType)
     37 		}
     38 	}
     39 	periodType, err := CommonValueType(periodTypes)
     40 	if err != nil {
     41 		return fmt.Errorf("period type: %v", err)
     42 	}
     43 
     44 	// Identify common sample types
     45 	numSampleTypes := len(profiles[0].SampleType)
     46 	for _, p := range profiles[1:] {
     47 		if numSampleTypes != len(p.SampleType) {
     48 			return fmt.Errorf("inconsistent samples type count: %d != %d", numSampleTypes, len(p.SampleType))
     49 		}
     50 	}
     51 	sampleType := make([]*profile.ValueType, numSampleTypes)
     52 	for i := 0; i < numSampleTypes; i++ {
     53 		sampleTypes := make([]*profile.ValueType, len(profiles))
     54 		for j, p := range profiles {
     55 			sampleTypes[j] = p.SampleType[i]
     56 		}
     57 		sampleType[i], err = CommonValueType(sampleTypes)
     58 		if err != nil {
     59 			return fmt.Errorf("sample types: %v", err)
     60 		}
     61 	}
     62 
     63 	for _, p := range profiles {
     64 		if p.PeriodType != nil && periodType != nil {
     65 			period, _ := Scale(p.Period, p.PeriodType.Unit, periodType.Unit)
     66 			p.Period, p.PeriodType.Unit = int64(period), periodType.Unit
     67 		}
     68 		ratios := make([]float64, len(p.SampleType))
     69 		for i, st := range p.SampleType {
     70 			if sampleType[i] == nil {
     71 				ratios[i] = 1
     72 				continue
     73 			}
     74 			ratios[i], _ = Scale(1, st.Unit, sampleType[i].Unit)
     75 			p.SampleType[i].Unit = sampleType[i].Unit
     76 		}
     77 		if err := p.ScaleN(ratios); err != nil {
     78 			return fmt.Errorf("scale: %v", err)
     79 		}
     80 	}
     81 	return nil
     82 }
     83 
     84 // CommonValueType returns the finest type from a set of compatible
     85 // types.
     86 func CommonValueType(ts []*profile.ValueType) (*profile.ValueType, error) {
     87 	if len(ts) <= 1 {
     88 		return nil, nil
     89 	}
     90 	minType := ts[0]
     91 	for _, t := range ts[1:] {
     92 		if !compatibleValueTypes(minType, t) {
     93 			return nil, fmt.Errorf("incompatible types: %v %v", *minType, *t)
     94 		}
     95 		if ratio, _ := Scale(1, t.Unit, minType.Unit); ratio < 1 {
     96 			minType = t
     97 		}
     98 	}
     99 	rcopy := *minType
    100 	return &rcopy, nil
    101 }
    102 
    103 func compatibleValueTypes(v1, v2 *profile.ValueType) bool {
    104 	if v1 == nil || v2 == nil {
    105 		return true // No grounds to disqualify.
    106 	}
    107 	// Remove trailing 's' to permit minor mismatches.
    108 	if t1, t2 := strings.TrimSuffix(v1.Type, "s"), strings.TrimSuffix(v2.Type, "s"); t1 != t2 {
    109 		return false
    110 	}
    111 
    112 	return v1.Unit == v2.Unit ||
    113 		(isTimeUnit(v1.Unit) && isTimeUnit(v2.Unit)) ||
    114 		(isMemoryUnit(v1.Unit) && isMemoryUnit(v2.Unit))
    115 }
    116 
    117 // Scale a measurement from an unit to a different unit and returns
    118 // the scaled value and the target unit. The returned target unit
    119 // will be empty if uninteresting (could be skipped).
    120 func Scale(value int64, fromUnit, toUnit string) (float64, string) {
    121 	// Avoid infinite recursion on overflow.
    122 	if value < 0 && -value > 0 {
    123 		v, u := Scale(-value, fromUnit, toUnit)
    124 		return -v, u
    125 	}
    126 	if m, u, ok := memoryLabel(value, fromUnit, toUnit); ok {
    127 		return m, u
    128 	}
    129 	if t, u, ok := timeLabel(value, fromUnit, toUnit); ok {
    130 		return t, u
    131 	}
    132 	// Skip non-interesting units.
    133 	switch toUnit {
    134 	case "count", "sample", "unit", "minimum", "auto":
    135 		return float64(value), ""
    136 	default:
    137 		return float64(value), toUnit
    138 	}
    139 }
    140 
    141 // Label returns the label used to describe a certain measurement.
    142 func Label(value int64, unit string) string {
    143 	return ScaledLabel(value, unit, "auto")
    144 }
    145 
    146 // ScaledLabel scales the passed-in measurement (if necessary) and
    147 // returns the label used to describe a float measurement.
    148 func ScaledLabel(value int64, fromUnit, toUnit string) string {
    149 	v, u := Scale(value, fromUnit, toUnit)
    150 	sv := strings.TrimSuffix(fmt.Sprintf("%.2f", v), ".00")
    151 	if sv == "0" || sv == "-0" {
    152 		return "0"
    153 	}
    154 	return sv + u
    155 }
    156 
    157 // isMemoryUnit returns whether a name is recognized as a memory size
    158 // unit.
    159 func isMemoryUnit(unit string) bool {
    160 	switch strings.TrimSuffix(strings.ToLower(unit), "s") {
    161 	case "byte", "b", "kilobyte", "kb", "megabyte", "mb", "gigabyte", "gb":
    162 		return true
    163 	}
    164 	return false
    165 }
    166 
    167 func memoryLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) {
    168 	fromUnit = strings.TrimSuffix(strings.ToLower(fromUnit), "s")
    169 	toUnit = strings.TrimSuffix(strings.ToLower(toUnit), "s")
    170 
    171 	switch fromUnit {
    172 	case "byte", "b":
    173 	case "kb", "kbyte", "kilobyte":
    174 		value *= 1024
    175 	case "mb", "mbyte", "megabyte":
    176 		value *= 1024 * 1024
    177 	case "gb", "gbyte", "gigabyte":
    178 		value *= 1024 * 1024 * 1024
    179 	case "tb", "tbyte", "terabyte":
    180 		value *= 1024 * 1024 * 1024 * 1024
    181 	case "pb", "pbyte", "petabyte":
    182 		value *= 1024 * 1024 * 1024 * 1024 * 1024
    183 	default:
    184 		return 0, "", false
    185 	}
    186 
    187 	if toUnit == "minimum" || toUnit == "auto" {
    188 		switch {
    189 		case value < 1024:
    190 			toUnit = "b"
    191 		case value < 1024*1024:
    192 			toUnit = "kb"
    193 		case value < 1024*1024*1024:
    194 			toUnit = "mb"
    195 		case value < 1024*1024*1024*1024:
    196 			toUnit = "gb"
    197 		case value < 1024*1024*1024*1024*1024:
    198 			toUnit = "tb"
    199 		default:
    200 			toUnit = "pb"
    201 		}
    202 	}
    203 
    204 	var output float64
    205 	switch toUnit {
    206 	default:
    207 		output, toUnit = float64(value), "B"
    208 	case "kb", "kbyte", "kilobyte":
    209 		output, toUnit = float64(value)/1024, "kB"
    210 	case "mb", "mbyte", "megabyte":
    211 		output, toUnit = float64(value)/(1024*1024), "MB"
    212 	case "gb", "gbyte", "gigabyte":
    213 		output, toUnit = float64(value)/(1024*1024*1024), "GB"
    214 	case "tb", "tbyte", "terabyte":
    215 		output, toUnit = float64(value)/(1024*1024*1024*1024), "TB"
    216 	case "pb", "pbyte", "petabyte":
    217 		output, toUnit = float64(value)/(1024*1024*1024*1024*1024), "PB"
    218 	}
    219 	return output, toUnit, true
    220 }
    221 
    222 // isTimeUnit returns whether a name is recognized as a time unit.
    223 func isTimeUnit(unit string) bool {
    224 	unit = strings.ToLower(unit)
    225 	if len(unit) > 2 {
    226 		unit = strings.TrimSuffix(unit, "s")
    227 	}
    228 
    229 	switch unit {
    230 	case "nanosecond", "ns", "microsecond", "millisecond", "ms", "s", "second", "sec", "hr", "day", "week", "year":
    231 		return true
    232 	}
    233 	return false
    234 }
    235 
    236 func timeLabel(value int64, fromUnit, toUnit string) (v float64, u string, ok bool) {
    237 	fromUnit = strings.ToLower(fromUnit)
    238 	if len(fromUnit) > 2 {
    239 		fromUnit = strings.TrimSuffix(fromUnit, "s")
    240 	}
    241 
    242 	toUnit = strings.ToLower(toUnit)
    243 	if len(toUnit) > 2 {
    244 		toUnit = strings.TrimSuffix(toUnit, "s")
    245 	}
    246 
    247 	var d time.Duration
    248 	switch fromUnit {
    249 	case "nanosecond", "ns":
    250 		d = time.Duration(value) * time.Nanosecond
    251 	case "microsecond":
    252 		d = time.Duration(value) * time.Microsecond
    253 	case "millisecond", "ms":
    254 		d = time.Duration(value) * time.Millisecond
    255 	case "second", "sec", "s":
    256 		d = time.Duration(value) * time.Second
    257 	case "cycle":
    258 		return float64(value), "", true
    259 	default:
    260 		return 0, "", false
    261 	}
    262 
    263 	if toUnit == "minimum" || toUnit == "auto" {
    264 		switch {
    265 		case d < 1*time.Microsecond:
    266 			toUnit = "ns"
    267 		case d < 1*time.Millisecond:
    268 			toUnit = "us"
    269 		case d < 1*time.Second:
    270 			toUnit = "ms"
    271 		case d < 1*time.Minute:
    272 			toUnit = "sec"
    273 		case d < 1*time.Hour:
    274 			toUnit = "min"
    275 		case d < 24*time.Hour:
    276 			toUnit = "hour"
    277 		case d < 15*24*time.Hour:
    278 			toUnit = "day"
    279 		case d < 120*24*time.Hour:
    280 			toUnit = "week"
    281 		default:
    282 			toUnit = "year"
    283 		}
    284 	}
    285 
    286 	var output float64
    287 	dd := float64(d)
    288 	switch toUnit {
    289 	case "ns", "nanosecond":
    290 		output, toUnit = dd/float64(time.Nanosecond), "ns"
    291 	case "us", "microsecond":
    292 		output, toUnit = dd/float64(time.Microsecond), "us"
    293 	case "ms", "millisecond":
    294 		output, toUnit = dd/float64(time.Millisecond), "ms"
    295 	case "min", "minute":
    296 		output, toUnit = dd/float64(time.Minute), "mins"
    297 	case "hour", "hr":
    298 		output, toUnit = dd/float64(time.Hour), "hrs"
    299 	case "day":
    300 		output, toUnit = dd/float64(24*time.Hour), "days"
    301 	case "week", "wk":
    302 		output, toUnit = dd/float64(7*24*time.Hour), "wks"
    303 	case "year", "yr":
    304 		output, toUnit = dd/float64(365*24*time.Hour), "yrs"
    305 	default:
    306 		fallthrough
    307 	case "sec", "second", "s":
    308 		output, toUnit = dd/float64(time.Second), "s"
    309 	}
    310 	return output, toUnit, true
    311 }
    312