Skip to content

Commit 74d7719

Browse files
committed
Merge pull request google#427 from rjnagal/master
Add percentiles utility methods.
2 parents 63fcd77 + de0e8e2 commit 74d7719

File tree

2 files changed

+279
-0
lines changed

2 files changed

+279
-0
lines changed

utils/percentiles.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright 2015 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 utils
16+
17+
import (
18+
"math"
19+
"sort"
20+
"time"
21+
22+
"github.com/golang/glog"
23+
"github.com/google/cadvisor/info"
24+
)
25+
26+
const milliSecondsToNanoSeconds = 1000000
27+
const secondsToMilliSeconds = 1000
28+
29+
type uint64Slice []uint64
30+
31+
func (a uint64Slice) Len() int { return len(a) }
32+
func (a uint64Slice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
33+
func (a uint64Slice) Less(i, j int) bool { return a[i] < a[j] }
34+
35+
// TODO(rjnagal): Move out when we update API.
36+
type Percentiles struct {
37+
// Average over the collected sample.
38+
Mean uint64 `json:"mean"`
39+
// Max seen over the collected sample.
40+
Max uint64 `json:"max"`
41+
// 90th percentile over the collected sample.
42+
Ninety uint64 `json:"ninety"`
43+
}
44+
45+
// Get 90th percentile of the provided samples. Round to integer.
46+
func (self uint64Slice) Get90Percentile() uint64 {
47+
count := self.Len()
48+
if count == 0 {
49+
return 0
50+
}
51+
sort.Sort(self)
52+
n := float64(0.9 * (float64(count) + 1))
53+
idx, frac := math.Modf(n)
54+
index := int(idx)
55+
percentile := float64(self[index-1])
56+
if index > 1 || index < count {
57+
percentile += frac * float64(self[index]-self[index-1])
58+
}
59+
return uint64(percentile)
60+
}
61+
62+
type Mean struct {
63+
// current count.
64+
count uint64
65+
// current mean.
66+
Mean float64
67+
}
68+
69+
func (self *Mean) Add(value uint64) {
70+
self.count++
71+
if self.count == 1 {
72+
self.Mean = float64(value)
73+
return
74+
}
75+
c := float64(self.count)
76+
v := float64(value)
77+
self.Mean = (self.Mean*(c-1) + v) / c
78+
}
79+
80+
// Returns cpu and memory usage percentiles.
81+
func GetPercentiles(stats []*info.ContainerStats) (Percentiles, Percentiles) {
82+
lastCpu := uint64(0)
83+
lastTime := time.Time{}
84+
memorySamples := make(uint64Slice, 0, len(stats))
85+
cpuSamples := make(uint64Slice, 0, len(stats)-1)
86+
numSamples := 0
87+
memoryMean := Mean{count: 0, Mean: 0}
88+
cpuMean := Mean{count: 0, Mean: 0}
89+
memoryPercentiles := Percentiles{}
90+
cpuPercentiles := Percentiles{}
91+
for _, stat := range stats {
92+
var elapsed int64
93+
time := stat.Timestamp
94+
if !lastTime.IsZero() {
95+
elapsed = time.UnixNano() - lastTime.UnixNano()
96+
if elapsed < 10*milliSecondsToNanoSeconds {
97+
glog.Infof("Elapsed time too small: %d ns: time now %s last %s", elapsed, time.String(), lastTime.String())
98+
continue
99+
}
100+
}
101+
numSamples++
102+
cpuNs := stat.Cpu.Usage.Total
103+
// Ignore actual usage and only focus on working set.
104+
memory := stat.Memory.WorkingSet
105+
if memory > memoryPercentiles.Max {
106+
memoryPercentiles.Max = memory
107+
}
108+
glog.V(2).Infof("Read sample: cpu %d, memory %d", cpuNs, memory)
109+
memoryMean.Add(memory)
110+
memorySamples = append(memorySamples, memory)
111+
if lastTime.IsZero() {
112+
lastCpu = cpuNs
113+
lastTime = time
114+
continue
115+
}
116+
cpuRate := (cpuNs - lastCpu) * secondsToMilliSeconds / uint64(elapsed)
117+
if cpuRate < 0 {
118+
glog.Infof("cpu rate too small: %f ns", cpuRate)
119+
continue
120+
}
121+
glog.V(2).Infof("Adding cpu rate sample : %d", cpuRate)
122+
lastCpu = cpuNs
123+
lastTime = time
124+
cpuSamples = append(cpuSamples, cpuRate)
125+
if cpuRate > cpuPercentiles.Max {
126+
cpuPercentiles.Max = cpuRate
127+
}
128+
cpuMean.Add(cpuRate)
129+
}
130+
cpuPercentiles.Mean = uint64(cpuMean.Mean)
131+
memoryPercentiles.Mean = uint64(memoryMean.Mean)
132+
cpuPercentiles.Ninety = cpuSamples.Get90Percentile()
133+
memoryPercentiles.Ninety = memorySamples.Get90Percentile()
134+
return cpuPercentiles, memoryPercentiles
135+
}

utils/percentiles_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2015 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 utils
16+
17+
import (
18+
"testing"
19+
"time"
20+
21+
"github.com/google/cadvisor/info"
22+
)
23+
24+
const Nanosecond = 1000000000
25+
26+
func Test90Percentile(t *testing.T) {
27+
N := 100
28+
stats := make(uint64Slice, 0, N)
29+
for i := N; i > 0; i-- {
30+
stats = append(stats, uint64(i))
31+
}
32+
p := stats.Get90Percentile()
33+
if p != 90 {
34+
t.Errorf("90th percentile is %d, should be 90.", p)
35+
}
36+
// 90p should be between 94 and 95. Promoted to 95.
37+
N = 105
38+
for i := 101; i <= N; i++ {
39+
stats = append(stats, uint64(i))
40+
}
41+
p = stats.Get90Percentile()
42+
if p != 95 {
43+
t.Errorf("90th percentile is %d, should be 95.", p)
44+
}
45+
}
46+
47+
func TestMean(t *testing.T) {
48+
var i, N uint64
49+
N = 100
50+
mean := Mean{count: 0, Mean: 0}
51+
for i = 1; i < N; i++ {
52+
mean.Add(i)
53+
}
54+
if mean.Mean != 50.0 {
55+
t.Errorf("Mean is %f, should be 50.0", mean.Mean)
56+
}
57+
}
58+
59+
func TestAggregates(t *testing.T) {
60+
N := uint64(100)
61+
var i uint64
62+
ct := time.Now()
63+
stats := make([]*info.ContainerStats, 0, N)
64+
for i = 1; i < N; i++ {
65+
s := &info.ContainerStats{
66+
Cpu: info.CpuStats{},
67+
Timestamp: ct.Add(time.Duration(i) * time.Second),
68+
Memory: info.MemoryStats{
69+
// Memory grows by a KB every second.
70+
WorkingSet: i * 1024,
71+
},
72+
}
73+
// cpu rate is 1 s/s
74+
s.Cpu.Usage.Total = i * Nanosecond
75+
stats = append(stats, s)
76+
}
77+
cpu, mem := GetPercentiles(stats)
78+
// Cpu mean, max, and 90p should all be 1000 ms/s.
79+
cpuExpected := Percentiles{
80+
Mean: 1000,
81+
Max: 1000,
82+
Ninety: 1000,
83+
}
84+
if cpu != cpuExpected {
85+
t.Errorf("cpu stats are %+v. Expected %+v", cpu, cpuExpected)
86+
}
87+
memExpected := Percentiles{
88+
Mean: 50 * 1024,
89+
Max: 99 * 1024,
90+
Ninety: 90 * 1024,
91+
}
92+
if mem != memExpected {
93+
t.Errorf("memory stats are mean %+v. Expected %+v", mem, memExpected)
94+
}
95+
}
96+
func TestSamplesCloseInTimeIgnored(t *testing.T) {
97+
N := uint64(100)
98+
var i uint64
99+
ct := time.Now()
100+
stats := make([]*info.ContainerStats, 0, N*2)
101+
for i = 1; i < N; i++ {
102+
s1 := &info.ContainerStats{
103+
Cpu: info.CpuStats{},
104+
Timestamp: ct.Add(time.Duration(i) * time.Second),
105+
Memory: info.MemoryStats{
106+
// Memory grows by a KB every second.
107+
WorkingSet: i * 1024,
108+
},
109+
}
110+
// cpu rate is 1 s/s
111+
s1.Cpu.Usage.Total = i * Nanosecond
112+
stats = append(stats, s1)
113+
114+
// Add another dummy sample too close in time to the last one.
115+
s2 := &info.ContainerStats{
116+
Cpu: info.CpuStats{},
117+
// Add extra millisecond.
118+
Timestamp: ct.Add(time.Duration(i) * time.Second).Add(time.Duration(1) * time.Millisecond),
119+
Memory: info.MemoryStats{
120+
WorkingSet: i * 1024 * 1024,
121+
},
122+
}
123+
s2.Cpu.Usage.Total = i * 100 * Nanosecond
124+
stats = append(stats, s2)
125+
}
126+
cpu, mem := GetPercentiles(stats)
127+
// Cpu mean, max, and 90p should all be 1000 ms/s. All high-value samples are discarded.
128+
cpuExpected := Percentiles{
129+
Mean: 1000,
130+
Max: 1000,
131+
Ninety: 1000,
132+
}
133+
if cpu != cpuExpected {
134+
t.Errorf("cpu stats are %+v. Expected %+v", cpu, cpuExpected)
135+
}
136+
memExpected := Percentiles{
137+
Mean: 50 * 1024,
138+
Max: 99 * 1024,
139+
Ninety: 90 * 1024,
140+
}
141+
if mem != memExpected {
142+
t.Errorf("memory stats are mean %+v. Expected %+v", mem, memExpected)
143+
}
144+
}

0 commit comments

Comments
 (0)