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