Skip to content

Commit 6a38f7b

Browse files
author
oleg.shokin
committed
CAP-2634 | fix: extract EMA smoother to utils
1 parent 0a19831 commit 6a38f7b

2 files changed

Lines changed: 91 additions & 51 deletions

File tree

controller/nextgc/component_p.go

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,49 @@ package nextgc
99
import (
1010
"fmt"
1111
"math"
12-
"sync"
1312

1413
"github.com/go-logr/logr"
14+
"github.com/newcloudtechnologies/memlimiter/utils"
1515
)
1616

1717
// The proportional component of the controller.
1818
type componentP struct {
19-
valueSmoother *emaSmoother
20-
cfg *ComponentProportionalConfig
21-
logger logr.Logger
19+
// valueSmoother is a smoother for the raw proportional signal.
20+
valueSmoother *utils.EMASmoother
21+
// cfg is the configuration for the proportional component.
22+
cfg *ComponentProportionalConfig
23+
// logger is the logger for the proportional component.
24+
logger logr.Logger
2225
}
2326

27+
// newComponentP creates a new proportional component.
28+
func newComponentP(logger logr.Logger, cfg *ComponentProportionalConfig) *componentP {
29+
out := &componentP{
30+
logger: logger,
31+
cfg: cfg,
32+
}
33+
34+
if cfg.WindowSize != 0 {
35+
// We smooth the raw proportional signal because memory usage is noisy:
36+
// short spikes should not immediately trigger aggressive control actions.
37+
//
38+
// EMA formula:
39+
// S_t = alpha*X_t + (1-alpha)*S_{t-1}
40+
//
41+
// To approximate a simple moving average window of size N, we use:
42+
// alpha = 2 / (N + 1)
43+
//
44+
// Larger window -> smaller alpha -> smoother but slower reaction.
45+
//nolint:gomnd
46+
alpha := 2 / (float64(cfg.WindowSize + 1))
47+
48+
out.valueSmoother = utils.NewEMASmoother(alpha)
49+
}
50+
51+
return out
52+
}
53+
54+
// value returns the proportional component's output.
2455
func (c *componentP) value(utilization float64) (float64, error) {
2556
if c.valueSmoother != nil {
2657
valueEMA, err := c.valueEMA(utilization)
@@ -39,6 +70,7 @@ func (c *componentP) value(utilization float64) (float64, error) {
3970
return valueRaw, nil
4071
}
4172

73+
// valueRaw returns the raw proportional component's output.
4274
func (c *componentP) valueRaw(utilization float64) (float64, error) {
4375
if utilization < 0 {
4476
return math.NaN(), fmt.Errorf("value is undefined if memory usage = %v", utilization)
@@ -62,6 +94,7 @@ func (c *componentP) valueRaw(utilization float64) (float64, error) {
6294
return c.cfg.Coefficient * (1 / (1 - utilization)), nil
6395
}
6496

97+
// valueEMA returns the exponential moving average of the raw proportional component's output.
6598
func (c *componentP) valueEMA(utilization float64) (float64, error) {
6699
valueRaw, err := c.valueRaw(utilization)
67100
if err != nil {
@@ -70,50 +103,3 @@ func (c *componentP) valueEMA(utilization float64) (float64, error) {
70103

71104
return c.valueSmoother.Update(valueRaw), nil
72105
}
73-
74-
func newComponentP(logger logr.Logger, cfg *ComponentProportionalConfig) *componentP {
75-
out := &componentP{
76-
logger: logger,
77-
cfg: cfg,
78-
}
79-
80-
if cfg.WindowSize != 0 {
81-
// alpha is a smoothing coefficient describing the degree of weighting decrease;
82-
// the lesser the alpha is, the higher the impact of the elder historical values on the resulting value.
83-
// alpha is choosed empirically, but can depend on a window size, like here:
84-
// https://en.wikipedia.org/wiki/Moving_average#Relationship_between_SMA_and_EMA
85-
//nolint:gomnd
86-
alpha := 2 / (float64(cfg.WindowSize + 1))
87-
88-
out.valueSmoother = newEMASmoother(alpha)
89-
}
90-
91-
return out
92-
}
93-
94-
type emaSmoother struct {
95-
mu sync.Mutex
96-
alpha float64
97-
initialized bool
98-
value float64
99-
}
100-
101-
func newEMASmoother(alpha float64) *emaSmoother {
102-
return &emaSmoother{alpha: alpha}
103-
}
104-
105-
func (e *emaSmoother) Update(v float64) float64 {
106-
e.mu.Lock()
107-
defer e.mu.Unlock()
108-
109-
if !e.initialized {
110-
e.value = v
111-
e.initialized = true
112-
113-
return e.value
114-
}
115-
116-
e.value = e.alpha*v + (1-e.alpha)*e.value
117-
118-
return e.value
119-
}

utils/ema.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) New Cloud Technologies, Ltd. 2013-2022.
3+
* Author: Vitaly Isaev <vitaly.isaev@myoffice.team>
4+
* License: https://github.com/newcloudtechnologies/memlimiter/blob/master/LICENSE
5+
*/
6+
7+
package utils
8+
9+
import "sync"
10+
11+
// EMASmoother is a concurrency-safe exponential moving average calculator.
12+
//
13+
// It helps stabilize noisy measurements (for example, memory utilization)
14+
// so control logic reacts to trend changes instead of short spikes.
15+
type EMASmoother struct {
16+
// alpha is the smoothing coefficient.
17+
alpha float64
18+
// initialized is a flag indicating if the smoother has been initialized.
19+
initialized bool
20+
// value is the current smoothed value.
21+
value float64
22+
// mu is a mutex for synchronization.
23+
mu sync.Mutex
24+
}
25+
26+
// NewEMASmoother creates a new exponential moving average calculator.
27+
//
28+
// The smoothing coefficient alpha is usually in (0; 1]:
29+
// smaller alpha -> smoother but slower reaction,
30+
// larger alpha -> faster reaction but noisier output.
31+
func NewEMASmoother(alpha float64) *EMASmoother {
32+
return &EMASmoother{alpha: alpha}
33+
}
34+
35+
// Update adds a new sample and returns the current smoothed value.
36+
//
37+
// Formula:
38+
//
39+
// S_t = alpha*X_t + (1-alpha)*S_{t-1}
40+
func (e *EMASmoother) Update(value float64) float64 {
41+
e.mu.Lock()
42+
defer e.mu.Unlock()
43+
44+
if !e.initialized {
45+
e.value = value
46+
e.initialized = true
47+
48+
return e.value
49+
}
50+
51+
e.value = e.alpha*value + (1-e.alpha)*e.value
52+
53+
return e.value
54+
}

0 commit comments

Comments
 (0)