@@ -265,6 +265,52 @@ func TestHistogramLoadFromCheckpointReturnsErrorOnNilInput(t *testing.T) {
265
265
assert .Error (t , err )
266
266
}
267
267
268
+ func TestHistogramIsNotEmptyAfterSavingAndLoadingCheckpointsWithBoundaryValues (t * testing.T ) {
269
+ // There is a specific scenario in which the weights of the minimum and maximum histogram buckets,
270
+ // when saved to a VPACheckpoint and subsequently loaded, result in diminished weights for the minimum buckets.
271
+
272
+ // This issue arises due to rounding errors when converting float weights to integers in the VPACheckpoint.
273
+ // For instance, consider the weights:
274
+ // `w1` which approximates but is slightly larger than or equal to `epsilon`,
275
+ // `w2` which approximates but is slightly smaller than or equal to (`MaxCheckpointWeight` * `epsilon`) - `epsilon`.
276
+
277
+ // When these weights are stored in a VPACheckpoint, they are translated to integers:
278
+ // `w1` rounds to `1` (`wi1`),
279
+ // `w2` rounds to `MaxCheckpointWeight` (`wi2`).
280
+
281
+ // Upon loading from the VPACheckpoint, the histogram reconstructs its weights using a calculated ratio,
282
+ // aimed at reverting integer weights back to float values. This ratio is derived from:
283
+ // (`w1` + `w2`) / (`wi1` + `wi2`)
284
+ // Reference: https://github.com/kubernetes/autoscaler/blob/aa1d413ea3bf319b56c7b2e65ade1a028e149439/vertical-pod-autoscaler//pkg/recommender/util/histogram.go#L256-L269
285
+
286
+ // Given the maximum potential values for `w1`, `w2`, `wi1` and `wi2` we arrive at:
287
+ // (`epsilon` + `MaxCheckpointWeight` * `epsilon` - `epsilon`) / (1 + MaxCheckpointWeight) = epsilon * `MaxCheckpointWeight` / (1 + MaxCheckpointWeight)
288
+
289
+ // Consequently, the maximum value for this ratio is less than `epsilon`, implying that when `w1`,
290
+ // initially scaled to `1`, is adjusted by this ratio, its recalculated weight falls short of `epsilon`.
291
+ // When the `minBucket`'s weight is less than `epsilon`, the `histogram.IsEmpty()` returns true.
292
+ // Reference: https://github.com/kubernetes/autoscaler/blob/aa1d413ea3bf319b56c7b2e65ade1a028e149439/vertical-pod-autoscaler/pkg/recommender/util/histogram.go#L181-L183
293
+ // Consequently, the `histogram.Percentile(...)` function will always return 0.
294
+ // Reference: https://github.com/kubernetes/autoscaler/blob/aa1d413ea3bf319b56c7b2e65ade1a028e149439/vertical-pod-autoscaler/pkg/recommender/util/histogram.go#L159-L162
295
+ // The same behavior can be observed when there are more than two weights.
296
+
297
+ // This test ensures that in such cases the histogram does not become empty.
298
+ // For more information check https://github.com/kubernetes/autoscaler/issues/7726
299
+
300
+ histogram := NewHistogram (testHistogramOptions )
301
+ histogram .AddSample (1 , weightEpsilon , anyTime )
302
+ histogram .AddSample (2 , (float64 (MaxCheckpointWeight )* weightEpsilon - weightEpsilon ), anyTime )
303
+ assert .False (t , histogram .IsEmpty ())
304
+
305
+ checkpoint , err := histogram .SaveToChekpoint ()
306
+ assert .NoError (t , err )
307
+
308
+ newHistogram := NewHistogram (testHistogramOptions )
309
+ err = newHistogram .LoadFromCheckpoint (checkpoint )
310
+ assert .NoError (t , err )
311
+ assert .False (t , newHistogram .IsEmpty ())
312
+ }
313
+
268
314
func areUnique (values ... interface {}) bool {
269
315
dict := make (map [interface {}]bool )
270
316
for i , v := range values {
0 commit comments