1818
1919#[ cfg( target_os = "linux" ) ]
2020use std:: path:: { Path , PathBuf } ;
21+ #[ cfg( target_os = "linux" ) ]
22+ use std:: sync:: Mutex ;
2123use std:: sync:: { Arc , LazyLock , atomic:: AtomicBool } ;
24+ #[ cfg( target_os = "linux" ) ]
25+ use std:: time:: Instant ;
2226
2327use actix_web:: {
2428 body:: MessageBody ,
@@ -35,19 +39,36 @@ use tokio::{
3539use tracing:: { info, trace, warn} ;
3640
3741use crate :: analytics:: { SYS_INFO , refresh_sys_info} ;
38- use crate :: metrics:: { record_disk_metrics, record_process_metrics_sample} ;
42+ use crate :: metrics:: {
43+ record_disk_metrics, record_process_cpu_usage_cores, record_process_metrics_sample,
44+ } ;
3945use crate :: parseable:: PARSEABLE ;
4046
4147const PROCESS_METRICS_SAMPLE_INTERVAL : Duration = Duration :: from_secs ( 5 ) ;
4248#[ cfg( target_os = "linux" ) ]
4349const CGROUP_V2_CPU_MAX_FILE : & str = "cpu.max" ;
4450#[ cfg( target_os = "linux" ) ]
51+ const CGROUP_V2_CPU_STAT_FILE : & str = "cpu.stat" ;
52+ #[ cfg( target_os = "linux" ) ]
4553const CGROUP_V1_CPU_QUOTA_FILE : & str = "cpu.cfs_quota_us" ;
4654#[ cfg( target_os = "linux" ) ]
4755const CGROUP_V1_CPU_PERIOD_FILE : & str = "cpu.cfs_period_us" ;
56+ #[ cfg( target_os = "linux" ) ]
57+ const CGROUP_V1_CPU_USAGE_FILE : & str = "cpuacct.usage" ;
4858
4959static SERVER_OK : LazyLock < Arc < AtomicBool > > = LazyLock :: new ( || Arc :: new ( AtomicBool :: new ( true ) ) ) ;
5060
61+ #[ cfg( target_os = "linux" ) ]
62+ #[ derive( Clone , Copy ) ]
63+ struct CgroupCpuUsageSample {
64+ usage_micros : u64 ,
65+ sampled_at : Instant ,
66+ }
67+
68+ #[ cfg( target_os = "linux" ) ]
69+ static CGROUP_CPU_USAGE_SAMPLE : LazyLock < Mutex < Option < CgroupCpuUsageSample > > > =
70+ LazyLock :: new ( || Mutex :: new ( None ) ) ;
71+
5172#[ cfg( target_os = "linux" ) ]
5273fn cpu_quota_cores ( quota : & str , period : & str ) -> Result < Option < f64 > , ( ) > {
5374 let quota = quota. trim ( ) ;
@@ -107,6 +128,82 @@ fn cgroup_cpu_limit_cores() -> Result<Option<f64>, ()> {
107128 cpu_quota_cores ( & quota, & period)
108129}
109130
131+ #[ cfg( target_os = "linux" ) ]
132+ fn cpu_usage_micros_from_stat ( cpu_stat : & str ) -> Result < u64 , ( ) > {
133+ for line in cpu_stat. lines ( ) {
134+ let mut fields = line. split_whitespace ( ) ;
135+ if fields. next ( ) == Some ( "usage_usec" ) {
136+ return fields. next ( ) . ok_or ( ( ) ) ?. parse :: < u64 > ( ) . map_err ( |_| ( ) ) ;
137+ }
138+ }
139+ Err ( ( ) )
140+ }
141+
142+ #[ cfg( target_os = "linux" ) ]
143+ fn cgroup_cpu_usage_micros ( ) -> Result < u64 , ( ) > {
144+ let process = procfs:: process:: Process :: myself ( ) . map_err ( |_| ( ) ) ?;
145+ let cgroups = process. cgroups ( ) . map_err ( |_| ( ) ) ?. 0 ;
146+ let mounts = process. mountinfo ( ) . map_err ( |_| ( ) ) ?. 0 ;
147+
148+ if let ( Some ( cgroup) , Some ( mount) ) = (
149+ cgroups. iter ( ) . find ( |group| group. hierarchy == 0 ) ,
150+ mounts. iter ( ) . find ( |mount| mount. fs_type == "cgroup2" ) ,
151+ ) {
152+ let directory =
153+ cgroup_directory ( & cgroup. pathname , & mount. root , & mount. mount_point ) . ok_or ( ( ) ) ?;
154+ let cpu_stat =
155+ std:: fs:: read_to_string ( directory. join ( CGROUP_V2_CPU_STAT_FILE ) ) . map_err ( |_| ( ) ) ?;
156+ return cpu_usage_micros_from_stat ( & cpu_stat) ;
157+ }
158+
159+ let cgroup = cgroups
160+ . iter ( )
161+ . find ( |group| group. controllers . iter ( ) . any ( |item| item == "cpuacct" ) )
162+ . ok_or ( ( ) ) ?;
163+ let mount = mounts
164+ . iter ( )
165+ . find ( |mount| mount. fs_type == "cgroup" && mount. super_options . contains_key ( "cpuacct" ) )
166+ . ok_or ( ( ) ) ?;
167+ let directory = cgroup_directory ( & cgroup. pathname , & mount. root , & mount. mount_point ) . ok_or ( ( ) ) ?;
168+ let usage_nanos = std:: fs:: read_to_string ( directory. join ( CGROUP_V1_CPU_USAGE_FILE ) )
169+ . map_err ( |_| ( ) ) ?
170+ . trim ( )
171+ . parse :: < u64 > ( )
172+ . map_err ( |_| ( ) ) ?;
173+ Ok ( usage_nanos / 1_000 )
174+ }
175+
176+ #[ cfg( target_os = "linux" ) ]
177+ fn cpu_usage_cores_between (
178+ previous_usage_micros : u64 ,
179+ current_usage_micros : u64 ,
180+ elapsed_micros : u128 ,
181+ ) -> Option < f64 > {
182+ let usage_delta = current_usage_micros. checked_sub ( previous_usage_micros) ?;
183+ ( elapsed_micros > 0 ) . then_some ( usage_delta as f64 / elapsed_micros as f64 )
184+ }
185+
186+ #[ cfg( target_os = "linux" ) ]
187+ fn cgroup_cpu_usage_cores ( ) -> Result < Option < f64 > , ( ) > {
188+ let current = CgroupCpuUsageSample {
189+ usage_micros : cgroup_cpu_usage_micros ( ) ?,
190+ sampled_at : Instant :: now ( ) ,
191+ } ;
192+ let mut previous = CGROUP_CPU_USAGE_SAMPLE . lock ( ) . map_err ( |_| ( ) ) ?;
193+ let usage_cores = previous. and_then ( |previous| {
194+ cpu_usage_cores_between (
195+ previous. usage_micros ,
196+ current. usage_micros ,
197+ current
198+ . sampled_at
199+ . duration_since ( previous. sampled_at )
200+ . as_micros ( ) ,
201+ )
202+ } ) ;
203+ * previous = Some ( current) ;
204+ Ok ( usage_cores)
205+ }
206+
110207#[ cfg( not( target_os = "linux" ) ) ]
111208fn cgroup_cpu_limit_cores ( ) -> Result < Option < f64 > , ( ) > {
112209 Err ( ( ) )
@@ -120,6 +217,18 @@ pub fn cpu_limit_cores() -> f64 {
120217 }
121218}
122219
220+ fn cpu_usage_cores ( ) -> f64 {
221+ #[ cfg( target_os = "linux" ) ]
222+ {
223+ return cgroup_cpu_usage_cores ( ) . ok ( ) . flatten ( ) . unwrap_or_default ( ) ;
224+ }
225+
226+ #[ cfg( not( target_os = "linux" ) ) ]
227+ {
228+ 0.0
229+ }
230+ }
231+
123232async fn sample_process_metrics ( ) {
124233 refresh_sys_info ( ) ;
125234 let process_metrics = tokio:: task:: spawn_blocking ( || {
@@ -137,13 +246,22 @@ async fn sample_process_metrics() {
137246 } ;
138247
139248 process_metrics. map ( |( cpu_usage, memory_bytes, total_mem) | {
140- ( cpu_usage, memory_bytes, total_mem, cpu_limit_cores ( ) )
249+ (
250+ cpu_usage,
251+ memory_bytes,
252+ total_mem,
253+ cpu_limit_cores ( ) ,
254+ cpu_usage_cores ( ) ,
255+ )
141256 } )
142257 } )
143258 . await
144259 . unwrap ( ) ;
145- if let Some ( ( cpu_usage, memory_bytes, total_mem, cpu_limit_cores) ) = process_metrics {
260+ if let Some ( ( cpu_usage, memory_bytes, total_mem, cpu_limit_cores, cpu_usage_cores) ) =
261+ process_metrics
262+ {
146263 record_process_metrics_sample ( cpu_usage, memory_bytes, total_mem, cpu_limit_cores) ;
264+ record_process_cpu_usage_cores ( cpu_usage_cores) ;
147265 }
148266
149267 let staging_path = PARSEABLE . options . staging_dir ( ) . clone ( ) ;
@@ -254,7 +372,7 @@ pub async fn check_resource_utilization_middleware(
254372
255373#[ cfg( all( test, target_os = "linux" ) ) ]
256374mod tests {
257- use super :: cpu_quota_cores;
375+ use super :: { cpu_quota_cores, cpu_usage_cores_between , cpu_usage_micros_from_stat } ;
258376
259377 #[ test]
260378 fn parses_limited_and_unlimited_cpu_quotas ( ) {
@@ -268,4 +386,25 @@ mod tests {
268386 assert_eq ! ( cpu_quota_cores( "invalid" , "100000" ) , Err ( ( ) ) ) ;
269387 assert_eq ! ( cpu_quota_cores( "50000" , "0" ) , Err ( ( ) ) ) ;
270388 }
389+
390+ #[ test]
391+ fn parses_cgroup_v2_cpu_usage ( ) {
392+ assert_eq ! (
393+ cpu_usage_micros_from_stat( "usage_usec 10200000\n user_usec 8000000\n " ) ,
394+ Ok ( 10_200_000 )
395+ ) ;
396+ assert_eq ! ( cpu_usage_micros_from_stat( "user_usec 8000000\n " ) , Err ( ( ) ) ) ;
397+ }
398+
399+ #[ test]
400+ fn calculates_cpu_usage_in_cores ( ) {
401+ assert_eq ! (
402+ cpu_usage_cores_between( 10_000_000 , 10_200_000 , 5_000_000 ) ,
403+ Some ( 0.04 )
404+ ) ;
405+ assert_eq ! (
406+ cpu_usage_cores_between( 10_200_000 , 10_000_000 , 5_000_000 ) ,
407+ None
408+ ) ;
409+ }
271410}
0 commit comments