Skip to content

Commit cde14ce

Browse files
committed
fix: correct the computation of the percentage differences and stats
Fix the computation of the CPU/Memory difference columns to express the result as the increase/decrease of the request in terms of the recommendation. Print the memory recommendation quantity using the same scale as the request in a human-friendly format. Scale the statistics values to the number of replicas requested in the spec of each controller.
1 parent 18a7886 commit cde14ce

File tree

7 files changed

+190
-90
lines changed

7 files changed

+190
-90
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ It creates a single binary file for the host machine platform/architecture in th
4343

4444
## Usage
4545

46+
### How to interpret the output?
47+
48+
The columns `% CPU DIFF` and `% MEMORY DIFF` represents the percentage of increase/decrease for the request in terms of the recommendation. For example, if the request is 4 CPU (4000m), and the recommendation is only 1 CPU (1000m), the difference is +300%. As a rule of thumb, you can think of positive values as "overcommitment" and negative values as "under commitment".
49+
4650
### Demo
4751

4852
The following examples were produced from a brand-new Kubernetes cluster created with [`k3d`](https://k3d.io/v5.2.2/). The `VerticalPodAutoscaler` resources were automatically created by the [`goldilocks`](https://github.com/FairwindsOps/goldilocks) operator.

cli/command.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ func newTableRow(v *vpav1.VerticalPodAutoscaler, tc *vpa.TargetController, name
272272
Namespace: v.Namespace,
273273
GVK: v.GroupVersionKind(),
274274
Mode: updateModeFromSpec(v.Spec.UpdatePolicy),
275+
Target: tc,
275276
TargetName: tc.Name,
276277
TargetGVK: tc.GroupVersionKind,
277278
Requests: rqs,

cli/table.go

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import (
88
"sort"
99
"strings"
1010

11-
"github.com/dustin/go-humanize"
1211
"github.com/muesli/termenv"
1312
"github.com/olekukonko/tablewriter"
1413
"gopkg.in/inf.v0"
1514
"k8s.io/apimachinery/pkg/api/resource"
1615
"k8s.io/apimachinery/pkg/runtime/schema"
1716

17+
"github.com/wI2L/kubectl-vpa-recommendation/internal/humanize"
1818
"github.com/wI2L/kubectl-vpa-recommendation/vpa"
1919
)
2020

@@ -54,8 +54,10 @@ type tableRow struct {
5454
Namespace string
5555
GVK schema.GroupVersionKind
5656
Mode string
57+
Target *vpa.TargetController
5758
TargetName string
5859
TargetGVK schema.GroupVersionKind
60+
TargetReplicas int32
5961
Requests vpa.ResourceQuantities
6062
Recommendations vpa.ResourceQuantities
6163
CPUDifference *float64
@@ -88,18 +90,30 @@ func (tr tableRow) toTableData(flags *Flags, isChild bool) []string {
8890

8991
if flags.wide {
9092
rowData = append(rowData,
91-
formatQuantity(tr.Requests.CPU),
92-
formatQuantity(tr.Recommendations.CPU),
93+
formatQuantity(tr.Requests.CPU), formatQuantity(tr.Recommendations.CPU),
9394
)
9495
}
9596
rowData = append(rowData, formatPercentage(tr.CPUDifference, flags.NoColors))
9697
if flags.wide {
98+
var str string
99+
d := inf.Dec{}
100+
d.Round(tr.Recommendations.Memory.AsDec(), 0, inf.RoundUp)
101+
b := d.UnscaledBig()
102+
switch tr.Requests.Memory.Format {
103+
case resource.DecimalSI:
104+
str = humanize.BigBytes(b, 2)
105+
case resource.BinarySI:
106+
str = humanize.BigIBytes(b, 2)
107+
default:
108+
str = tr.Recommendations.Memory.String()
109+
}
97110
rowData = append(rowData,
98111
formatQuantity(tr.Requests.Memory),
99-
formatQuantity(tr.Recommendations.Memory),
112+
str,
100113
)
101114
}
102115
rowData = append(rowData, formatPercentage(tr.MemoryDifference, flags.NoColors))
116+
103117
return rowData
104118
}
105119

@@ -191,35 +205,48 @@ func (t table) printStats(w io.Writer) error {
191205
t.medianQuantities,
192206
}
193207
rows := []struct {
194-
name string
195-
getter func(i int) *resource.Quantity
196-
asBytes bool
208+
name string
209+
quantityGetter func(i int) *resource.Quantity
210+
asBytes bool
197211
}{
198-
{"CPU Recommendations (# cores)", func(i int) *resource.Quantity { return t[i].Recommendations.CPU }, false},
199212
{"CPU Requests (# cores)", func(i int) *resource.Quantity { return t[i].Requests.CPU }, false},
200-
{"MEM Recommendations (IEC/SI)", func(i int) *resource.Quantity { return t[i].Recommendations.Memory }, true},
213+
{"CPU Recommendations (# cores)", func(i int) *resource.Quantity { return t[i].Recommendations.CPU }, false},
201214
{"MEM Requests (IEC/SI)", func(i int) *resource.Quantity { return t[i].Requests.Memory }, true},
215+
{"MEM Recommendations (IEC/SI)", func(i int) *resource.Quantity { return t[i].Recommendations.Memory }, true},
202216
}
203217
for _, row := range rows {
204218
values := make([]string, 0, len(statFuncs))
219+
205220
for _, fn := range statFuncs {
206-
q := fn(row.getter)
221+
scaledQuantity := func(i int) *resource.Quantity {
222+
q := row.quantityGetter(i)
223+
224+
// Scale the quantity according to the number
225+
// of replicas declared in the controller's spec.
226+
var replicas int64
227+
n, err := t[i].Target.ReplicasCount()
228+
if err != nil {
229+
replicas = 1
230+
} else {
231+
replicas = n
232+
}
233+
return multiplyQuantity(q, replicas)
234+
}
235+
q := fn(scaledQuantity)
207236

208-
var str string
209-
if q == nil {
210-
str = tableUnsetCell
211-
} else {
237+
s := tableUnsetCell
238+
if q != nil {
212239
if row.asBytes {
213-
tmp := inf.Dec{}
214-
tmp.Round(q.AsDec(), 0, inf.RoundUp)
215-
big := tmp.UnscaledBig()
216-
str = humanize.BigIBytes(big) + "/" + humanize.BigBytes(big)
217-
str = strings.ReplaceAll(str, " ", "")
240+
d := inf.Dec{}
241+
d.Round(q.AsDec(), 0, inf.RoundUp)
242+
b := d.UnscaledBig()
243+
s = humanize.BigIBytes(b, 2) + "/" + humanize.BigBytes(b, 2)
244+
s = strings.ReplaceAll(s, " ", "")
218245
} else {
219-
str = q.AsDec().String()
246+
s = q.AsDec().Round(q.AsDec(), 2, inf.RoundCeil).String()
220247
}
221248
}
222-
values = append(values, str)
249+
values = append(values, s)
223250
}
224251
tw.Append(append([]string{row.name}, values...))
225252
}
@@ -229,6 +256,23 @@ func (t table) printStats(w io.Writer) error {
229256
return nil
230257
}
231258

259+
func multiplyQuantity(q *resource.Quantity, n int64) *resource.Quantity {
260+
if q == nil || n == 0 {
261+
return nil
262+
}
263+
if n == 1 {
264+
return q
265+
}
266+
// The resource.Quantity type does not define a
267+
// multiplication method, so instead we add the
268+
// same amount n-1 times.
269+
ret := q.DeepCopy()
270+
for i := 0; int64(i) < n-1; i++ {
271+
ret.Add(*q)
272+
}
273+
return &ret
274+
}
275+
232276
func (t table) sumQuantities(column func(i int) *resource.Quantity) *resource.Quantity {
233277
var sum resource.Quantity
234278
for i := range t {
@@ -279,7 +323,7 @@ func (t table) medianQuantities(column func(i int) *resource.Quantity) *resource
279323
q := values[l/2-1]
280324
q.Add(*(values[l/2]))
281325
tmp := inf.Dec{}
282-
tmp.QuoRound(q.AsDec(), inf.NewDec(2, 0), 3, inf.RoundDown)
326+
tmp.QuoRound(q.AsDec(), inf.NewDec(2, 0), 2, inf.RoundUp)
283327

284328
return resource.NewDecimalQuantity(tmp, resource.DecimalSI)
285329
}

internal/humanize/bigbytes.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package humanize
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
)
7+
8+
var (
9+
bigSIExp = big.NewInt(1000)
10+
bigIECExp = big.NewInt(1024)
11+
)
12+
13+
// BigBytes produces a human-readable representation of an SI size.
14+
func BigBytes(s *big.Int, precision int) string {
15+
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
16+
return humanizeBigBytes(s, bigSIExp, sizes, precision)
17+
}
18+
19+
// BigIBytes produces a human-readable representation of an IEC size.
20+
func BigIBytes(s *big.Int, precision int) string {
21+
sizes := []string{"B", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi", "Yi"}
22+
return humanizeBigBytes(s, bigIECExp, sizes, precision)
23+
}
24+
25+
var ten = big.NewInt(10)
26+
27+
func humanizeBigBytes(s, base *big.Int, sizes []string, precision int) string {
28+
if s.Cmp(ten) < 0 {
29+
return fmt.Sprintf("%d B", s)
30+
}
31+
c := (&big.Int{}).Set(s)
32+
val, mag := orderOfMagnitude(c, base, len(sizes)-1)
33+
suffix := sizes[mag]
34+
f := "%.0f %s"
35+
if val < 10 {
36+
f = fmt.Sprintf("%%.%df %%s", precision)
37+
}
38+
return fmt.Sprintf(f, val, suffix)
39+
}
40+
41+
func orderOfMagnitude(n, b *big.Int, max int) (float64, int) {
42+
mag := 0
43+
m := &big.Int{}
44+
for n.Cmp(b) >= 0 {
45+
n.DivMod(n, b, m)
46+
mag++
47+
if mag == max && max >= 0 {
48+
break
49+
}
50+
}
51+
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
52+
}

vpa/resource.go

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@ package vpa
22

33
import (
44
"math"
5-
"strconv"
65

7-
"gopkg.in/inf.v0"
86
"k8s.io/apimachinery/pkg/api/resource"
97
)
108

11-
const precision = 2
12-
139
// ResourceQuantities is a pair of resource quantities
1410
// that can represent the recommendations of a VerticalPodAutoscaler
1511
// of the requests of a pod's container.
@@ -18,39 +14,18 @@ type ResourceQuantities struct {
1814
Memory *resource.Quantity
1915
}
2016

21-
// DiffQuantitiesAsPercent return the difference between
22-
// two quantities expressed as a percentage of each other,
23-
// where x is the request and y the recommendation.
24-
func DiffQuantitiesAsPercent(x, y *resource.Quantity) *float64 {
25-
if x == nil || y == nil || x.IsZero() || y.IsZero() {
17+
// DiffQuantitiesAsPercent return the difference between two
18+
// quantities. The return value is expressed as the increase/decrease
19+
// of the request in terms of the recommendation.
20+
func DiffQuantitiesAsPercent(req, rec *resource.Quantity) *float64 {
21+
if req == nil || rec == nil || req.IsZero() || rec.IsZero() {
2622
return nil
2723
}
28-
ai, oka := x.AsInt64()
29-
bi, okb := y.AsInt64()
30-
31-
if oka && okb {
32-
f := (float64(ai) - float64(bi)) / float64(ai) * 100
33-
exp := powFloat64(10, precision)
34-
// Round down to 'precision' decimal places.
35-
f = math.Floor(f*exp) / exp
36-
return &f
37-
}
38-
ad := x.AsDec()
39-
bd := y.AsDec()
40-
41-
p := &inf.Dec{}
42-
p.Sub(ad, bd)
43-
p.QuoRound(p, ad, precision+precision, inf.RoundDown)
44-
p.Mul(p, inf.NewDec(100, 0))
45-
p.Round(p, precision, inf.RoundDown)
24+
xf := req.AsApproximateFloat64()
25+
yf := rec.AsApproximateFloat64()
4626

47-
f, err := strconv.ParseFloat(p.String(), 64)
48-
if err != nil {
49-
return nil
50-
}
51-
return &f
52-
}
27+
p := (xf - yf) / yf * 100.0
28+
p = math.Round(p*100) / 100 // round nearest
5329

54-
func powFloat64(x, y int) float64 {
55-
return math.Pow(float64(x), float64(y))
30+
return &p
5631
}

vpa/resource_test.go

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,60 @@ import (
99

1010
func TestDiffQuantitiesAsPercent(t *testing.T) {
1111
testCases := []struct {
12-
X *resource.Quantity
13-
Y *resource.Quantity
14-
P *float64
12+
Req *resource.Quantity
13+
Rec *resource.Quantity
14+
P *float64
1515
}{
1616
{
17-
X: resource.NewQuantity(0, resource.DecimalSI),
18-
Y: resource.NewQuantity(25, resource.DecimalSI),
19-
P: nil,
17+
Req: resource.NewQuantity(4, resource.DecimalSI),
18+
Rec: resource.NewQuantity(1, resource.DecimalSI),
19+
P: pointer.Float64(300.00),
2020
},
2121
{
22-
X: resource.NewQuantity(25, resource.DecimalSI),
23-
Y: resource.NewQuantity(0, resource.DecimalSI),
24-
P: nil,
22+
Req: resource.NewQuantity(100, resource.DecimalSI),
23+
Rec: resource.NewQuantity(25, resource.DecimalSI),
24+
P: pointer.Float64(300.00),
2525
},
2626
{
27-
X: resource.NewQuantity(100, resource.DecimalSI),
28-
Y: resource.NewQuantity(25, resource.DecimalSI),
29-
P: pointer.Float64(75.00),
27+
Req: resource.NewQuantity(444, resource.BinarySI),
28+
Rec: resource.NewQuantity(1000, resource.BinarySI),
29+
P: pointer.Float64(-55.60),
3030
},
3131
{
32-
X: resource.NewQuantity(444, resource.BinarySI),
33-
Y: resource.NewQuantity(1000, resource.BinarySI),
34-
P: pointer.Float64(-125.23),
32+
Req: resource.NewQuantity(1000, resource.BinarySI),
33+
Rec: resource.NewQuantity(444, resource.BinarySI),
34+
P: pointer.Float64(125.23),
3535
},
3636
{
37-
X: resource.NewQuantity(1000, resource.BinarySI),
38-
Y: resource.NewQuantity(444, resource.BinarySI),
39-
P: pointer.Float64(55.60),
37+
Req: resource.NewScaledQuantity(8, resource.Peta),
38+
Rec: resource.NewScaledQuantity(24, resource.Peta),
39+
P: pointer.Float64(-66.67),
4040
},
4141
{
42-
X: resource.NewScaledQuantity(8, resource.Peta),
43-
Y: resource.NewScaledQuantity(24, resource.Peta),
44-
P: pointer.Float64(-200.00),
42+
Req: resource.NewScaledQuantity(128, resource.Exa),
43+
Rec: resource.NewScaledQuantity(512, resource.Exa),
44+
P: pointer.Float64(-75.00),
4545
},
4646
{
47-
X: resource.NewScaledQuantity(128, resource.Exa),
48-
Y: resource.NewScaledQuantity(512, resource.Exa),
49-
P: pointer.Float64(-300.00),
47+
Req: resource.NewScaledQuantity(666, resource.Giga),
48+
Rec: resource.NewScaledQuantity(32, resource.Exa),
49+
P: pointer.Float64(-100.00),
5050
},
5151
{
52-
X: resource.NewScaledQuantity(666, resource.Giga),
53-
Y: resource.NewScaledQuantity(32, resource.Exa),
54-
P: pointer.Float64(-4804804704.80),
52+
Req: resource.NewScaledQuantity(32, resource.Exa),
53+
Rec: resource.NewScaledQuantity(666, resource.Giga),
54+
P: pointer.Float64(4804804704.80),
5555
},
5656
{
57-
X: resource.NewScaledQuantity(32, resource.Exa),
58-
Y: resource.NewScaledQuantity(666, resource.Giga),
59-
P: pointer.Float64(99.99),
57+
Req: resource.NewMilliQuantity(100, resource.DecimalSI),
58+
Rec: resource.NewMilliQuantity(25, resource.DecimalSI),
59+
P: pointer.Float64(300.00),
6060
},
6161
}
6262
for _, tc := range testCases {
63-
t.Logf("X: %s, Y: %s", tc.X, tc.Y)
63+
t.Logf("Req: %s, Rec: %s", tc.Req, tc.Rec)
6464

65-
p := DiffQuantitiesAsPercent(tc.X, tc.Y)
65+
p := DiffQuantitiesAsPercent(tc.Req, tc.Rec)
6666
if p == nil {
6767
if tc.P != nil {
6868
t.Errorf("got nil result, want %.2f", *tc.P)

0 commit comments

Comments
 (0)