Skip to content

Commit

Permalink
Merge pull request #7428 from omerap12/memory_humanize
Browse files Browse the repository at this point in the history
feat: humanize memory quantities using binary SI units
  • Loading branch information
k8s-ci-robot authored Nov 7, 2024
2 parents 0e85453 + 0f014bd commit 3abfbcb
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.V(4).InfoS("Converting raw value to humanized value", "rawValue", rawValues, "humanizedValue", humanizedValue)
quantity = resource.MustParse(humanizedValue)
}
default:
klog.ErrorS(nil, "Cannot translate resource name", "resourceName", 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 3abfbcb

Please sign in to comment.