@@ -68,6 +68,51 @@ pub(crate) fn generate_uptime(elapsed_secs: f64) -> Vec<u8> {
6868 format ! ( "{:.2} 0.00\n " , elapsed_secs. max( 0.0 ) ) . into_bytes ( )
6969}
7070
71+ // ============================================================
72+ // /proc/loadavg generator + EWMA tracker
73+ // ============================================================
74+
75+ /// Exponential weighted moving average load tracker, matching the Linux kernel's
76+ /// algorithm (kernel/sched/loadavg.c). Sampled every 5 seconds.
77+ #[ derive( Debug , Clone ) ]
78+ pub struct LoadAvg {
79+ pub avg_1 : f64 ,
80+ pub avg_5 : f64 ,
81+ pub avg_15 : f64 ,
82+ }
83+
84+ // Decay factors: e^(-5/60), e^(-5/300), e^(-5/900)
85+ const EXP_1 : f64 = 0.9200444146293232 ; // e^(-1/12)
86+ const EXP_5 : f64 = 0.9834714538216174 ; // e^(-1/60)
87+ const EXP_15 : f64 = 0.9944598480048967 ; // e^(-1/180)
88+
89+ impl LoadAvg {
90+ pub fn new ( ) -> Self {
91+ Self { avg_1 : 0.0 , avg_5 : 0.0 , avg_15 : 0.0 }
92+ }
93+
94+ /// Update averages with current runnable process count.
95+ /// Called every 5 seconds by the sampling task.
96+ pub fn sample ( & mut self , running : u32 ) {
97+ let r = running as f64 ;
98+ self . avg_1 = self . avg_1 * EXP_1 + r * ( 1.0 - EXP_1 ) ;
99+ self . avg_5 = self . avg_5 * EXP_5 + r * ( 1.0 - EXP_5 ) ;
100+ self . avg_15 = self . avg_15 * EXP_15 + r * ( 1.0 - EXP_15 ) ;
101+ }
102+ }
103+
104+ /// Generate /proc/loadavg from tracked EWMA values.
105+ /// Format: "avg1 avg5 avg15 running/total last_pid\n"
106+ pub ( crate ) fn generate_loadavg ( load : & LoadAvg , running : u32 , total : u32 , last_pid : i32 ) -> Vec < u8 > {
107+ format ! (
108+ "{:.2} {:.2} {:.2} {}/{} {}\n " ,
109+ load. avg_1, load. avg_5, load. avg_15,
110+ running. max( 1 ) . min( total) , total,
111+ last_pid. max( 0 ) ,
112+ )
113+ . into_bytes ( )
114+ }
115+
71116// /proc/meminfo generator
72117// ============================================================
73118
@@ -243,6 +288,16 @@ pub(crate) async fn handle_proc_open(
243288 return inject_memfd ( & content) ;
244289 }
245290
291+ // Virtualize /proc/loadavg when proc virtualization is active.
292+ if path == "/proc/loadavg" {
293+ let st = state. lock ( ) . await ;
294+ let total = st. proc_pids . len ( ) as u32 ;
295+ let running = st. proc_count ;
296+ let last_pid = st. proc_pids . iter ( ) . max ( ) . copied ( ) . unwrap_or ( 0 ) ;
297+ let content = generate_loadavg ( & st. load_avg , running, total, last_pid) ;
298+ return inject_memfd ( & content) ;
299+ }
300+
246301 // Virtualize /proc/net/tcp and /proc/net/tcp6 when port_remap is active.
247302 if policy. port_remap && ( path == "/proc/net/tcp" || path == "/proc/net/tcp6" ) {
248303 let is_v6 = path. ends_with ( '6' ) ;
@@ -703,6 +758,59 @@ mod tests {
703758 assert ! ( text. starts_with( "0.00" ) ) ;
704759 }
705760
761+ #[ test]
762+ fn test_loadavg_ewma ( ) {
763+ let mut la = LoadAvg :: new ( ) ;
764+ assert_eq ! ( la. avg_1, 0.0 ) ;
765+ assert_eq ! ( la. avg_5, 0.0 ) ;
766+ assert_eq ! ( la. avg_15, 0.0 ) ;
767+
768+ // After sampling with 4 running processes, averages should rise
769+ for _ in 0 ..12 {
770+ la. sample ( 4 ) ;
771+ }
772+ // 1-min average should converge faster than 5 and 15
773+ assert ! ( la. avg_1 > la. avg_5) ;
774+ assert ! ( la. avg_5 > la. avg_15) ;
775+ assert ! ( la. avg_1 > 2.0 ) ; // should be well above 0 after 60s of load=4
776+ }
777+
778+ #[ test]
779+ fn test_loadavg_ewma_decay ( ) {
780+ let mut la = LoadAvg :: new ( ) ;
781+ // Load up
782+ for _ in 0 ..60 {
783+ la. sample ( 10 ) ;
784+ }
785+ let peak = la. avg_1 ;
786+ // Load drops to 0
787+ for _ in 0 ..60 {
788+ la. sample ( 0 ) ;
789+ }
790+ assert ! ( la. avg_1 < peak * 0.1 , "1-min avg should decay quickly" ) ;
791+ }
792+
793+ #[ test]
794+ fn test_generate_loadavg ( ) {
795+ let la = LoadAvg { avg_1 : 1.23 , avg_5 : 0.45 , avg_15 : 0.12 } ;
796+ let info = generate_loadavg ( & la, 3 , 10 , 42 ) ;
797+ let text = String :: from_utf8 ( info) . unwrap ( ) ;
798+ assert ! ( text. contains( "1.23" ) ) ;
799+ assert ! ( text. contains( "0.45" ) ) ;
800+ assert ! ( text. contains( "0.12" ) ) ;
801+ assert ! ( text. contains( "3/10" ) ) ;
802+ assert ! ( text. contains( "42" ) ) ;
803+ }
804+
805+ #[ test]
806+ fn test_generate_loadavg_zero_procs ( ) {
807+ let la = LoadAvg :: new ( ) ;
808+ let info = generate_loadavg ( & la, 0 , 0 , 0 ) ;
809+ let text = String :: from_utf8 ( info) . unwrap ( ) ;
810+ // running should be clamped: max(0,1).min(0) = 0
811+ assert ! ( text. contains( "0/0" ) ) ;
812+ }
813+
706814 #[ test]
707815 fn test_build_dirent64 ( ) {
708816 let entry = build_dirent64 ( 12345 , 1 , DT_DIR , "1234" ) ;
0 commit comments