Skip to content

Commit

Permalink
feat: humanize memory quantities using binary SI units
Browse files Browse the repository at this point in the history
Signed-off-by: Omer Aplatony <[email protected]>
  • Loading branch information
omerap12 committed Oct 28, 2024
1 parent 068ce78 commit ab6d986
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 5 deletions.
9 changes: 5 additions & 4 deletions vertical-pod-autoscaler/pkg/recommender/logic/recommender.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var (
targetMemoryPercentile = flag.Float64("target-memory-percentile", 0.9, "Memory usage percentile that will be used as a base for memory target recommendation. Doesn't affect memory lower bound nor memory upper bound.")
lowerBoundMemoryPercentile = flag.Float64("recommendation-lower-bound-memory-percentile", 0.5, `Memory usage percentile that will be used for the lower bound on memory recommendation.`)
upperBoundMemoryPercentile = flag.Float64("recommendation-upper-bound-memory-percentile", 0.95, `Memory usage percentile that will be used for the upper bound on memory recommendation.`)
humanizeMemory = flag.Bool("humanize-memory", false, "Convert memory values in recommendations to the highest appropriate SI unit with up to 2 decimal places for better readability.")
)

// PodResourceRecommender computes resource recommendation for a Vpa object.
Expand Down Expand Up @@ -164,10 +165,10 @@ func MapToListOfRecommendedContainerResources(resources RecommendedPodResources)
for _, name := range containerNames {
containerResources = append(containerResources, vpa_types.RecommendedContainerResources{
ContainerName: name,
Target: model.ResourcesAsResourceList(resources[name].Target),
LowerBound: model.ResourcesAsResourceList(resources[name].LowerBound),
UpperBound: model.ResourcesAsResourceList(resources[name].UpperBound),
UncappedTarget: model.ResourcesAsResourceList(resources[name].Target),
Target: model.ResourcesAsResourceList(resources[name].Target, *humanizeMemory),
LowerBound: model.ResourcesAsResourceList(resources[name].LowerBound, *humanizeMemory),
UpperBound: model.ResourcesAsResourceList(resources[name].UpperBound, *humanizeMemory),
UncappedTarget: model.ResourcesAsResourceList(resources[name].Target, *humanizeMemory),
})
}
recommendation := &vpa_types.RecommendedPodResources{
Expand Down
33 changes: 32 additions & 1 deletion vertical-pod-autoscaler/pkg/recommender/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package model

import (
"fmt"

apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/klog/v2"
Expand Down Expand Up @@ -79,7 +81,7 @@ func ScaleResource(amount ResourceAmount, factor float64) ResourceAmount {
}

// ResourcesAsResourceList converts internal Resources representation to ResourcesList.
func ResourcesAsResourceList(resources Resources) apiv1.ResourceList {
func ResourcesAsResourceList(resources Resources, humanizeMemory bool) apiv1.ResourceList {
result := make(apiv1.ResourceList)
for key, resourceAmount := range resources {
var newKey apiv1.ResourceName
Expand All @@ -91,6 +93,12 @@ func ResourcesAsResourceList(resources Resources) apiv1.ResourceList {
case ResourceMemory:
newKey = apiv1.ResourceMemory
quantity = QuantityFromMemoryAmount(resourceAmount)
if humanizeMemory && !quantity.IsZero() {
rawValues := quantity.Value()
humanizedValue := HumanizeMemoryQuantity(rawValues)
klog.InfoS("Converting raw value to humanized value", "rawValue", rawValues, "humanizedValue", humanizedValue)
quantity = resource.MustParse(humanizedValue)
}
default:
klog.Errorf("Cannot translate %v resource name", key)
continue
Expand Down Expand Up @@ -141,6 +149,29 @@ func resourceAmountFromFloat(amount float64) ResourceAmount {
}
}

// HumanizeMemoryQuantity converts raw bytes to human-readable string using binary units (KiB, MiB, GiB, TiB) with no decimal places.
func HumanizeMemoryQuantity(bytes int64) string {
const (
KiB = 1024
MiB = 1024 * KiB
GiB = 1024 * MiB
TiB = 1024 * GiB
)

switch {
case bytes >= TiB:
return fmt.Sprintf("%.2fTi", float64(bytes)/float64(TiB))
case bytes >= GiB:
return fmt.Sprintf("%.2fGi", float64(bytes)/float64(GiB))
case bytes >= MiB:
return fmt.Sprintf("%.2fMi", float64(bytes)/float64(MiB))
case bytes >= KiB:
return fmt.Sprintf("%.2fKi", float64(bytes)/float64(KiB))
default:
return fmt.Sprintf("%d", bytes)
}
}

// PodID contains information needed to identify a Pod within a cluster.
type PodID struct {
// Namespaces where the Pod is defined.
Expand Down
196 changes: 196 additions & 0 deletions vertical-pod-autoscaler/pkg/recommender/model/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package model

import (
"testing"

apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

type ResourcesAsResourceListTestCase struct {
name string
resources Resources
humanize bool
resourceList apiv1.ResourceList
}

func TestResourcesAsResourceList(t *testing.T) {
testCases := []ResourcesAsResourceListTestCase{
{
name: "basic resources without humanize",
resources: Resources{
ResourceCPU: 1000,
ResourceMemory: 1000,
},
humanize: false,
resourceList: apiv1.ResourceList{
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
apiv1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI),
},
},
{
name: "basic resources with humanize",
resources: Resources{
ResourceCPU: 1000,
ResourceMemory: 262144000, // 250Mi
},
humanize: true,
resourceList: apiv1.ResourceList{
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
apiv1.ResourceMemory: resource.MustParse("250.00Mi"),
},
},
{
name: "large memory value with humanize",
resources: Resources{
ResourceCPU: 1000,
ResourceMemory: 839500000, // 800.61Mi
},
humanize: true,
resourceList: apiv1.ResourceList{
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
apiv1.ResourceMemory: resource.MustParse("800.61Mi"),
},
},
{
name: "zero values without humanize",
resources: Resources{
ResourceCPU: 0,
ResourceMemory: 0,
},
humanize: false,
resourceList: apiv1.ResourceList{
apiv1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI),
apiv1.ResourceMemory: *resource.NewQuantity(0, resource.DecimalSI),
},
},
{
name: "large memory value without humanize",
resources: Resources{
ResourceCPU: 1000,
ResourceMemory: 839500000,
},
humanize: false,
resourceList: apiv1.ResourceList{
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
apiv1.ResourceMemory: *resource.NewQuantity(839500000, resource.DecimalSI),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := ResourcesAsResourceList(tc.resources, tc.humanize)
if !result[apiv1.ResourceCPU].Equal(tc.resourceList[apiv1.ResourceCPU]) {
t.Errorf("expected %v, got %v", tc.resourceList[apiv1.ResourceCPU], result[apiv1.ResourceCPU])
}
if !result[apiv1.ResourceMemory].Equal(tc.resourceList[apiv1.ResourceMemory]) {
t.Errorf("expected %v, got %v", tc.resourceList[apiv1.ResourceMemory], result[apiv1.ResourceMemory])
}
})
}
}

type HumanizeMemoryQuantityTestCase struct {
name string
value int64
wanted string
}

func TestHumanizeMemoryQuantity(t *testing.T) {
testCases := []HumanizeMemoryQuantityTestCase{
{
name: "1.00Ki",
value: 1024,
wanted: "1.00Ki",
},
{
name: "1.00Mi",
value: 1024 * 1024,
wanted: "1.00Mi",
},
{
name: "1.00Gi",
value: 1024 * 1024 * 1024,
wanted: "1.00Gi",
},
{
name: "1.00Ti",
value: 1024 * 1024 * 1024 * 1024,
wanted: "1.00Ti",
},
{
name: "256.00Mi",
value: 256 * 1024 * 1024,
wanted: "256.00Mi",
},
{
name: "1.50Gi",
value: 1.5 * 1024 * 1024 * 1024,
wanted: "1.50Gi",
},
{
name: "1Mi in bytes",
value: 1050000,
wanted: "1.00Mi",
},
{
name: "1.5Ki in bytes",
value: 1537,
wanted: "1.50Ki",
},
{
name: "4.65Gi",
value: 4992073454,
wanted: "4.65Gi",
},
{
name: "6.05Gi",
value: 6499152537,
wanted: "6.05Gi",
},
{
name: "15.23Gi",
value: 16357476492,
wanted: "15.23Gi",
},
{
name: "3.75Gi",
value: 4022251530,
wanted: "3.75Gi",
},
{
name: "12.65Gi",
value: 13580968030,
wanted: "12.65Gi",
},
{
name: "14.46Gi",
value: 15530468536,
wanted: "14.46Gi",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(*testing.T) {
result := HumanizeMemoryQuantity(tc.value)
if result != tc.wanted {
t.Errorf("expected %v, got %v", tc.wanted, result)
}
})
}
}

0 comments on commit ab6d986

Please sign in to comment.