@@ -21,6 +21,9 @@ internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser
21
21
{
22
22
private const int Thousand = 1000 ;
23
23
private const int CpuShares = 1024 ;
24
+ private const string CpuStat = "cpu.stat" ; // File containing CPU usage in nanoseconds.
25
+ private const string CpuLimit = "cpu.max" ; // File with amount of CPU time available to the group along with the accounting period in microseconds.
26
+ private const string CpuRequest = "cpu.weight" ; // CPU weights, also known as shares in cgroup v1, is used for resource allocation.
24
27
private static readonly ObjectPool < BufferWriter < char > > _sharedBufferWriterPool = BufferWriterPool . CreateBufferWriterPool < char > ( ) ;
25
28
26
29
/// <remarks>
@@ -86,47 +89,76 @@ internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser
86
89
/// </summary>
87
90
private static readonly FileInfo _cpuPodWeight = new ( "/sys/fs/cgroup/cpu.weight" ) ;
88
91
92
+ private static readonly FileInfo _cpuCgroupInfoFile = new ( "/proc/self/cgroup" ) ;
93
+
89
94
private readonly IFileSystem _fileSystem ;
90
95
private readonly long _userHz ;
91
96
97
+ // Cache for the trimmed path string to avoid repeated file reads and processing
98
+ private string ? _cachedCgroupPath ;
99
+
92
100
public LinuxUtilizationParserCgroupV2 ( IFileSystem fileSystem , IUserHz userHz )
93
101
{
94
102
_fileSystem = fileSystem ;
95
103
_userHz = userHz . Value ;
96
104
}
97
105
98
- public long GetCgroupCpuUsageInNanoseconds ( )
106
+ public string GetCgroupPath ( string filename )
99
107
{
100
- // The value we are interested in starts with this. We just want to make sure it is true.
101
- const string Usage_usec = "usage_usec" ;
102
-
103
- // If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat.
104
- if ( ! _fileSystem . Exists ( _cpuacctUsage ) )
108
+ // If we've already parsed the path, use the cached value
109
+ if ( _cachedCgroupPath != null )
105
110
{
106
- return GetHostCpuUsageInNanoseconds ( ) ;
111
+ return $ " { _cachedCgroupPath } { filename } " ;
107
112
}
108
113
109
114
using ReturnableBufferWriter < char > bufferWriter = new ( _sharedBufferWriterPool ) ;
110
- _fileSystem . ReadAll ( _cpuacctUsage , bufferWriter . Buffer ) ;
111
- ReadOnlySpan < char > usage = bufferWriter . Buffer . WrittenSpan ;
112
115
113
- if ( ! usage . StartsWith ( Usage_usec ) )
116
+ // Read the content of the file
117
+ _fileSystem . ReadFirstLine ( _cpuCgroupInfoFile , bufferWriter . Buffer ) ;
118
+ ReadOnlySpan < char > fileContent = bufferWriter . Buffer . WrittenSpan ;
119
+
120
+ // Ensure the file content is not empty
121
+ if ( fileContent . IsEmpty )
114
122
{
115
- Throw . InvalidOperationException ( $ "Could not parse ' { _cpuacctUsage } '. We expected first line of the file to start with ' { Usage_usec } ' but it was ' { new string ( usage ) } ' instead .") ;
123
+ Throw . InvalidOperationException ( $ "The file ' { _cpuCgroupInfoFile } ' is empty or could not be read .") ;
116
124
}
117
125
118
- ReadOnlySpan < char > cpuUsage = usage . Slice ( Usage_usec . Length , usage . Length - Usage_usec . Length ) ;
126
+ // Find the index of the first colon (:)
127
+ int colonIndex = fileContent . LastIndexOf ( ':' ) ;
128
+ if ( colonIndex == - 1 || colonIndex + 1 >= fileContent . Length )
129
+ {
130
+ Throw . InvalidOperationException ( $ "Invalid format in file '{ _cpuCgroupInfoFile } '. Expected content with ':' separator.") ;
131
+ }
119
132
120
- int next = GetNextNumber ( cpuUsage , out long microseconds ) ;
133
+ // Extract the part after the last colon and cache it for future use
134
+ ReadOnlySpan < char > trimmedPath = fileContent . Slice ( colonIndex + 1 ) ;
135
+ _cachedCgroupPath = "/sys/fs/cgroup" + trimmedPath . ToString ( ) . TrimEnd ( '/' ) + "/" ;
121
136
122
- if ( microseconds == - 1 )
137
+ return $ "{ _cachedCgroupPath } { filename } ";
138
+ }
139
+
140
+ public long GetCgroupCpuUsageInNanoseconds ( )
141
+ {
142
+ // If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat.
143
+ if ( ! _fileSystem . Exists ( _cpuacctUsage ) )
123
144
{
124
- Throw . InvalidOperationException ( $ "Could not get cpu usage from ' { _cpuacctUsage } '. Expected positive number, but got ' { new string ( usage ) } '." ) ;
145
+ return GetHostCpuUsageInNanoseconds ( ) ;
125
146
}
126
147
127
- // In cgroup v2, the Units are microseconds for usage_usec.
128
- // We multiply by 1000 to convert to nanoseconds to keep the common calculation logic.
129
- return microseconds * Thousand ;
148
+ return ParseCpuUsageFromFile ( _fileSystem , _cpuacctUsage ) ;
149
+ }
150
+
151
+ public long GetCgroupCpuUsageInNanosecondsV2 ( )
152
+ {
153
+ FileInfo cpuUsageFile = new ( GetCgroupPath ( CpuStat ) ) ;
154
+
155
+ // If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat.
156
+ if ( ! _fileSystem . Exists ( cpuUsageFile ) )
157
+ {
158
+ return GetHostCpuUsageInNanoseconds ( ) ;
159
+ }
160
+
161
+ return ParseCpuUsageFromFile ( _fileSystem , cpuUsageFile ) ;
130
162
}
131
163
132
164
public long GetHostCpuUsageInNanoseconds ( )
@@ -184,6 +216,22 @@ public float GetCgroupLimitedCpus()
184
216
return GetHostCpuCount ( ) ;
185
217
}
186
218
219
+ /// <remarks>
220
+ /// When CGroup limits are set, we can calculate number of cores based on the file settings.
221
+ /// It should be 99% of the cases when app is hosted in the container environment.
222
+ /// Otherwise, we assume that all host's CPUs are available, which we read from proc/stat file.
223
+ /// </remarks>
224
+ public float GetCgroupLimitV2 ( )
225
+ {
226
+ FileInfo cpuLimitsFile = new ( GetCgroupPath ( CpuLimit ) ) ;
227
+ if ( LinuxUtilizationParserCgroupV2 . TryGetCpuLimitFromCgroupsV2 ( _fileSystem , cpuLimitsFile , out float cpus ) )
228
+ {
229
+ return cpus ;
230
+ }
231
+
232
+ return GetHostCpuCount ( ) ;
233
+ }
234
+
187
235
/// <remarks>
188
236
/// If we are able to read the CPU share, we calculate the CPU request based on the weight by dividing it by 1024.
189
237
/// If we can't read the CPU weight, we assume that the pod/vm cpu request is 1 core by default.
@@ -198,6 +246,21 @@ public float GetCgroupRequestCpu()
198
246
return GetHostCpuCount ( ) ;
199
247
}
200
248
249
+ /// <remarks>
250
+ /// If we are able to read the CPU share, we calculate the CPU request based on the weight by dividing it by 1024.
251
+ /// If we can't read the CPU weight, we assume that the pod/vm cpu request is 1 core by default.
252
+ /// </remarks>
253
+ public float GetCgroupRequestCpuV2 ( )
254
+ {
255
+ FileInfo cpuRequestsFile = new ( GetCgroupPath ( CpuRequest ) ) ;
256
+ if ( TryGetCgroupRequestCpuV2 ( _fileSystem , cpuRequestsFile , out float cpuPodRequest ) )
257
+ {
258
+ return cpuPodRequest / CpuShares ;
259
+ }
260
+
261
+ return GetHostCpuCount ( ) ;
262
+ }
263
+
201
264
/// <remarks>
202
265
/// If the file doesn't exist, we assume that the system is a Host and we read the memory from /proc/meminfo.
203
266
/// </remarks>
@@ -447,6 +510,34 @@ static void ThrowException(ReadOnlySpan<char> content) =>
447
510
$ "Could not parse '{ _cpuSetCpus } '. Expected comma-separated list of integers, with dashes (\" -\" ) based ranges (\" 0\" , \" 2-6,12\" ) but got '{ new string ( content ) } '.") ;
448
511
}
449
512
513
+ private static long ParseCpuUsageFromFile ( IFileSystem fileSystem , FileInfo cpuUsageFile )
514
+ {
515
+ // The value we are interested in starts with this. We just want to make sure it is true.
516
+ const string Usage_usec = "usage_usec" ;
517
+
518
+ using ReturnableBufferWriter < char > bufferWriter = new ( _sharedBufferWriterPool ) ;
519
+ fileSystem . ReadAll ( cpuUsageFile , bufferWriter . Buffer ) ;
520
+ ReadOnlySpan < char > usage = bufferWriter . Buffer . WrittenSpan ;
521
+
522
+ if ( ! usage . StartsWith ( Usage_usec ) )
523
+ {
524
+ Throw . InvalidOperationException ( $ "Could not parse '{ cpuUsageFile } '. We expected first line of the file to start with '{ Usage_usec } ' but it was '{ new string ( usage ) } ' instead.") ;
525
+ }
526
+
527
+ ReadOnlySpan < char > cpuUsage = usage . Slice ( Usage_usec . Length , usage . Length - Usage_usec . Length ) ;
528
+
529
+ int next = GetNextNumber ( cpuUsage , out long microseconds ) ;
530
+
531
+ if ( microseconds == - 1 )
532
+ {
533
+ Throw . InvalidOperationException ( $ "Could not get cpu usage from '{ cpuUsageFile } '. Expected positive number, but got '{ new string ( usage ) } '.") ;
534
+ }
535
+
536
+ // In cgroup v2, the Units are microseconds for usage_usec.
537
+ // We multiply by 1000 to convert to nanoseconds to keep the common calculation logic.
538
+ return microseconds * Thousand ;
539
+ }
540
+
450
541
/// <remarks>
451
542
/// The input must contain only number. If there is something more than whitespace before the number, it will return failure (-1).
452
543
/// </remarks>
@@ -492,8 +583,27 @@ private static bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float
492
583
return false ;
493
584
}
494
585
586
+ return TryParseCpuQuotaAndPeriodFromFile ( fileSystem , _cpuCfsQuaotaPeriodUs , out cpuUnits ) ;
587
+ }
588
+
589
+ /// <remarks>
590
+ /// If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat.
591
+ /// </remarks>
592
+ private static bool TryGetCpuLimitFromCgroupsV2 ( IFileSystem fileSystem , FileInfo cpuLimitsFile , out float cpuUnits )
593
+ {
594
+ if ( ! fileSystem . Exists ( cpuLimitsFile ) )
595
+ {
596
+ cpuUnits = 0 ;
597
+ return false ;
598
+ }
599
+
600
+ return TryParseCpuQuotaAndPeriodFromFile ( fileSystem , cpuLimitsFile , out cpuUnits ) ;
601
+ }
602
+
603
+ private static bool TryParseCpuQuotaAndPeriodFromFile ( IFileSystem fileSystem , FileInfo cpuLimitsFile , out float cpuUnits )
604
+ {
495
605
using ReturnableBufferWriter < char > bufferWriter = new ( _sharedBufferWriterPool ) ;
496
- fileSystem . ReadFirstLine ( _cpuCfsQuaotaPeriodUs , bufferWriter . Buffer ) ;
606
+ fileSystem . ReadFirstLine ( cpuLimitsFile , bufferWriter . Buffer ) ;
497
607
498
608
ReadOnlySpan < char > quotaBuffer = bufferWriter . Buffer . WrittenSpan ;
499
609
@@ -513,7 +623,7 @@ private static bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float
513
623
514
624
if ( quota == - 1 )
515
625
{
516
- Throw . InvalidOperationException ( $ "Could not parse '{ _cpuCfsQuaotaPeriodUs } '. Expected an integer but got: '{ new string ( quotaBuffer ) } '.") ;
626
+ Throw . InvalidOperationException ( $ "Could not parse '{ cpuLimitsFile } '. Expected an integer but got: '{ new string ( quotaBuffer ) } '.") ;
517
627
}
518
628
519
629
string quotaString = quota . ToString ( CultureInfo . CurrentCulture ) ;
@@ -523,7 +633,7 @@ private static bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float
523
633
524
634
if ( period == - 1 )
525
635
{
526
- Throw . InvalidOperationException ( $ "Could not parse '{ _cpuCfsQuaotaPeriodUs } '. Expected to get an integer but got: '{ new string ( cpuPeriodSlice ) } '.") ;
636
+ Throw . InvalidOperationException ( $ "Could not parse '{ cpuLimitsFile } '. Expected to get an integer but got: '{ new string ( cpuPeriodSlice ) } '.") ;
527
637
}
528
638
529
639
cpuUnits = ( float ) quota / period ;
@@ -533,37 +643,53 @@ private static bool TryGetCpuUnitsFromCgroups(IFileSystem fileSystem, out float
533
643
534
644
private static bool TryGetCgroupRequestCpu ( IFileSystem fileSystem , out float cpuUnits )
535
645
{
536
- const long CpuPodWeightPossibleMax = 10_000 ;
537
- const long CpuPodWeightPossibleMin = 1 ;
538
-
539
646
if ( ! fileSystem . Exists ( _cpuPodWeight ) )
540
647
{
541
648
cpuUnits = 0 ;
542
649
return false ;
543
650
}
544
651
652
+ return TryParseCpuWeightFromFile ( fileSystem , _cpuPodWeight , out cpuUnits ) ;
653
+ }
654
+
655
+ private static bool TryGetCgroupRequestCpuV2 ( IFileSystem fileSystem , FileInfo cpuRequestsFile , out float cpuUnits )
656
+ {
657
+ if ( ! fileSystem . Exists ( cpuRequestsFile ) )
658
+ {
659
+ cpuUnits = 0 ;
660
+ return false ;
661
+ }
662
+
663
+ return TryParseCpuWeightFromFile ( fileSystem , cpuRequestsFile , out cpuUnits ) ;
664
+ }
665
+
666
+ private static bool TryParseCpuWeightFromFile ( IFileSystem fileSystem , FileInfo cpuWeightFile , out float cpuUnits )
667
+ {
668
+ const long CpuPodWeightPossibleMax = 10_000 ;
669
+ const long CpuPodWeightPossibleMin = 1 ;
670
+
545
671
using ReturnableBufferWriter < char > bufferWriter = new ( _sharedBufferWriterPool ) ;
546
- fileSystem . ReadFirstLine ( _cpuPodWeight , bufferWriter . Buffer ) ;
672
+ fileSystem . ReadFirstLine ( cpuWeightFile , bufferWriter . Buffer ) ;
547
673
ReadOnlySpan < char > cpuPodWeightBuffer = bufferWriter . Buffer . WrittenSpan ;
548
674
549
675
if ( cpuPodWeightBuffer . IsEmpty || ( cpuPodWeightBuffer . Length == 2 && cpuPodWeightBuffer [ 0 ] == '-' && cpuPodWeightBuffer [ 1 ] == '1' ) )
550
676
{
551
677
Throw . InvalidOperationException (
552
- $ "Could not parse '{ _cpuPodWeight } ' content. Expected to find CPU weight but got '{ new string ( cpuPodWeightBuffer ) } ' instead.") ;
678
+ $ "Could not parse '{ cpuWeightFile } ' content. Expected to find CPU weight but got '{ new string ( cpuPodWeightBuffer ) } ' instead.") ;
553
679
}
554
680
555
681
_ = GetNextNumber ( cpuPodWeightBuffer , out long cpuPodWeight ) ;
556
682
557
683
if ( cpuPodWeight == - 1 )
558
684
{
559
685
Throw . InvalidOperationException (
560
- $ "Could not parse '{ _cpuPodWeight } ' content. Expected to get an integer but got: '{ cpuPodWeightBuffer } '.") ;
686
+ $ "Could not parse '{ cpuWeightFile } ' content. Expected to get an integer but got: '{ cpuPodWeightBuffer } '.") ;
561
687
}
562
688
563
689
if ( cpuPodWeight < CpuPodWeightPossibleMin || cpuPodWeight > CpuPodWeightPossibleMax )
564
690
{
565
691
Throw . ArgumentOutOfRangeException ( "CPU weight" ,
566
- $ "Expected to find CPU weight in range [{ CpuPodWeightPossibleMin } -{ CpuPodWeightPossibleMax } ] in '{ _cpuPodWeight } ', but got '{ cpuPodWeight } ' instead.") ;
692
+ $ "Expected to find CPU weight in range [{ CpuPodWeightPossibleMin } -{ CpuPodWeightPossibleMax } ] in '{ cpuWeightFile } ', but got '{ cpuPodWeight } ' instead.") ;
567
693
}
568
694
569
695
// The formula to calculate CPU pod weight (measured in millicores) from CPU share:
0 commit comments