@@ -9,18 +9,49 @@ package nextgc
99import (
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.
1818type 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.
2455func (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.
4274func (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.
6598func (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- }
0 commit comments