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.
16package measurement
17
18import (
19	"fmt"
20	"math"
21	"strings"
22	"time"
23
24	"github.com/google/pprof/profile"
25)
26
27// ScaleProfiles updates the units in a set of profiles to make them
28// compatible. It scales the profiles to the smallest unit to preserve
29// data.
30func ScaleProfiles(profiles []*profile.Profile) error {
31	if len(profiles) == 0 {
32		return nil
33	}
34	periodTypes := make([]*profile.ValueType, 0, len(profiles))
35	for _, p := range profiles {
36		if p.PeriodType != nil {
37			periodTypes = append(periodTypes, p.PeriodType)
38		}
39	}
40	periodType, err := CommonValueType(periodTypes)
41	if err != nil {
42		return fmt.Errorf("period type: %v", err)
43	}
44
45	// Identify common sample types
46	numSampleTypes := len(profiles[0].SampleType)
47	for _, p := range profiles[1:] {
48		if numSampleTypes != len(p.SampleType) {
49			return fmt.Errorf("inconsistent samples type count: %d != %d", numSampleTypes, len(p.SampleType))
50		}
51	}
52	sampleType := make([]*profile.ValueType, numSampleTypes)
53	for i := 0; i < numSampleTypes; i++ {
54		sampleTypes := make([]*profile.ValueType, len(profiles))
55		for j, p := range profiles {
56			sampleTypes[j] = p.SampleType[i]
57		}
58		sampleType[i], err = CommonValueType(sampleTypes)
59		if err != nil {
60			return fmt.Errorf("sample types: %v", err)
61		}
62	}
63
64	for _, p := range profiles {
65		if p.PeriodType != nil && periodType != nil {
66			period, _ := Scale(p.Period, p.PeriodType.Unit, periodType.Unit)
67			p.Period, p.PeriodType.Unit = int64(period), periodType.Unit
68		}
69		ratios := make([]float64, len(p.SampleType))
70		for i, st := range p.SampleType {
71			if sampleType[i] == nil {
72				ratios[i] = 1
73				continue
74			}
75			ratios[i], _ = Scale(1, st.Unit, sampleType[i].Unit)
76			p.SampleType[i].Unit = sampleType[i].Unit
77		}
78		if err := p.ScaleN(ratios); err != nil {
79			return fmt.Errorf("scale: %v", err)
80		}
81	}
82	return nil
83}
84
85// CommonValueType returns the finest type from a set of compatible
86// types.
87func CommonValueType(ts []*profile.ValueType) (*profile.ValueType, error) {
88	if len(ts) <= 1 {
89		return nil, nil
90	}
91	minType := ts[0]
92	for _, t := range ts[1:] {
93		if !compatibleValueTypes(minType, t) {
94			return nil, fmt.Errorf("incompatible types: %v %v", *minType, *t)
95		}
96		if ratio, _ := Scale(1, t.Unit, minType.Unit); ratio < 1 {
97			minType = t
98		}
99	}
100	rcopy := *minType
101	return &rcopy, nil
102}
103
104func compatibleValueTypes(v1, v2 *profile.ValueType) bool {
105	if v1 == nil || v2 == nil {
106		return true // No grounds to disqualify.
107	}
108	// Remove trailing 's' to permit minor mismatches.
109	if t1, t2 := strings.TrimSuffix(v1.Type, "s"), strings.TrimSuffix(v2.Type, "s"); t1 != t2 {
110		return false
111	}
112
113	return v1.Unit == v2.Unit ||
114		(timeUnits.sniffUnit(v1.Unit) != nil && timeUnits.sniffUnit(v2.Unit) != nil) ||
115		(memoryUnits.sniffUnit(v1.Unit) != nil && memoryUnits.sniffUnit(v2.Unit) != nil) ||
116		(gcuUnits.sniffUnit(v1.Unit) != nil && gcuUnits.sniffUnit(v2.Unit) != nil)
117}
118
119// Scale a measurement from an unit to a different unit and returns
120// the scaled value and the target unit. The returned target unit
121// will be empty if uninteresting (could be skipped).
122func Scale(value int64, fromUnit, toUnit string) (float64, string) {
123	// Avoid infinite recursion on overflow.
124	if value < 0 && -value > 0 {
125		v, u := Scale(-value, fromUnit, toUnit)
126		return -v, u
127	}
128	if m, u, ok := memoryUnits.convertUnit(value, fromUnit, toUnit); ok {
129		return m, u
130	}
131	if t, u, ok := timeUnits.convertUnit(value, fromUnit, toUnit); ok {
132		return t, u
133	}
134	if g, u, ok := gcuUnits.convertUnit(value, fromUnit, toUnit); ok {
135		return g, u
136	}
137	// Skip non-interesting units.
138	switch toUnit {
139	case "count", "sample", "unit", "minimum", "auto":
140		return float64(value), ""
141	default:
142		return float64(value), toUnit
143	}
144}
145
146// Label returns the label used to describe a certain measurement.
147func Label(value int64, unit string) string {
148	return ScaledLabel(value, unit, "auto")
149}
150
151// ScaledLabel scales the passed-in measurement (if necessary) and
152// returns the label used to describe a float measurement.
153func ScaledLabel(value int64, fromUnit, toUnit string) string {
154	v, u := Scale(value, fromUnit, toUnit)
155	sv := strings.TrimSuffix(fmt.Sprintf("%.2f", v), ".00")
156	if sv == "0" || sv == "-0" {
157		return "0"
158	}
159	return sv + u
160}
161
162// Percentage computes the percentage of total of a value, and encodes
163// it as a string. At least two digits of precision are printed.
164func Percentage(value, total int64) string {
165	var ratio float64
166	if total != 0 {
167		ratio = math.Abs(float64(value)/float64(total)) * 100
168	}
169	switch {
170	case math.Abs(ratio) >= 99.95 && math.Abs(ratio) <= 100.05:
171		return "  100%"
172	case math.Abs(ratio) >= 1.0:
173		return fmt.Sprintf("%5.2f%%", ratio)
174	default:
175		return fmt.Sprintf("%5.2g%%", ratio)
176	}
177}
178
179// unit includes a list of aliases representing a specific unit and a factor
180// which one can multiple a value in the specified unit by to get the value
181// in terms of the base unit.
182type unit struct {
183	canonicalName string
184	aliases       []string
185	factor        float64
186}
187
188// unitType includes a list of units that are within the same category (i.e.
189// memory or time units) and a default unit to use for this type of unit.
190type unitType struct {
191	defaultUnit unit
192	units       []unit
193}
194
195// findByAlias returns the unit associated with the specified alias. It returns
196// nil if the unit with such alias is not found.
197func (ut unitType) findByAlias(alias string) *unit {
198	for _, u := range ut.units {
199		for _, a := range u.aliases {
200			if alias == a {
201				return &u
202			}
203		}
204	}
205	return nil
206}
207
208// sniffUnit simpifies the input alias and returns the unit associated with the
209// specified alias. It returns nil if the unit with such alias is not found.
210func (ut unitType) sniffUnit(unit string) *unit {
211	unit = strings.ToLower(unit)
212	if len(unit) > 2 {
213		unit = strings.TrimSuffix(unit, "s")
214	}
215	return ut.findByAlias(unit)
216}
217
218// autoScale takes in the value with units of the base unit and returns
219// that value scaled to a reasonable unit if a reasonable unit is
220// found.
221func (ut unitType) autoScale(value float64) (float64, string, bool) {
222	var f float64
223	var unit string
224	for _, u := range ut.units {
225		if u.factor >= f && (value/u.factor) >= 1.0 {
226			f = u.factor
227			unit = u.canonicalName
228		}
229	}
230	if f == 0 {
231		return 0, "", false
232	}
233	return value / f, unit, true
234}
235
236// convertUnit converts a value from the fromUnit to the toUnit, autoscaling
237// the value if the toUnit is "minimum" or "auto". If the fromUnit is not
238// included in the unitType, then a false boolean will be returned. If the
239// toUnit is not in the unitType, the value will be returned in terms of the
240// default unitType.
241func (ut unitType) convertUnit(value int64, fromUnitStr, toUnitStr string) (float64, string, bool) {
242	fromUnit := ut.sniffUnit(fromUnitStr)
243	if fromUnit == nil {
244		return 0, "", false
245	}
246	v := float64(value) * fromUnit.factor
247	if toUnitStr == "minimum" || toUnitStr == "auto" {
248		if v, u, ok := ut.autoScale(v); ok {
249			return v, u, true
250		}
251		return v / ut.defaultUnit.factor, ut.defaultUnit.canonicalName, true
252	}
253	toUnit := ut.sniffUnit(toUnitStr)
254	if toUnit == nil {
255		return v / ut.defaultUnit.factor, ut.defaultUnit.canonicalName, true
256	}
257	return v / toUnit.factor, toUnit.canonicalName, true
258}
259
260var memoryUnits = unitType{
261	units: []unit{
262		{"B", []string{"b", "byte"}, 1},
263		{"kB", []string{"kb", "kbyte", "kilobyte"}, float64(1 << 10)},
264		{"MB", []string{"mb", "mbyte", "megabyte"}, float64(1 << 20)},
265		{"GB", []string{"gb", "gbyte", "gigabyte"}, float64(1 << 30)},
266		{"TB", []string{"tb", "tbyte", "terabyte"}, float64(1 << 40)},
267		{"PB", []string{"pb", "pbyte", "petabyte"}, float64(1 << 50)},
268	},
269	defaultUnit: unit{"B", []string{"b", "byte"}, 1},
270}
271
272var timeUnits = unitType{
273	units: []unit{
274		{"ns", []string{"ns", "nanosecond"}, float64(time.Nanosecond)},
275		{"us", []string{"μs", "us", "microsecond"}, float64(time.Microsecond)},
276		{"ms", []string{"ms", "millisecond"}, float64(time.Millisecond)},
277		{"s", []string{"s", "sec", "second"}, float64(time.Second)},
278		{"hrs", []string{"hour", "hr"}, float64(time.Hour)},
279	},
280	defaultUnit: unit{"s", []string{}, float64(time.Second)},
281}
282
283var gcuUnits = unitType{
284	units: []unit{
285		{"n*GCU", []string{"nanogcu"}, 1e-9},
286		{"u*GCU", []string{"microgcu"}, 1e-6},
287		{"m*GCU", []string{"milligcu"}, 1e-3},
288		{"GCU", []string{"gcu"}, 1},
289		{"k*GCU", []string{"kilogcu"}, 1e3},
290		{"M*GCU", []string{"megagcu"}, 1e6},
291		{"G*GCU", []string{"gigagcu"}, 1e9},
292		{"T*GCU", []string{"teragcu"}, 1e12},
293		{"P*GCU", []string{"petagcu"}, 1e15},
294	},
295	defaultUnit: unit{"GCU", []string{}, 1.0},
296}
297