| | | 1 | | using System; |
| | | 2 | | using System.Diagnostics; |
| | | 3 | | using System.IO; |
| | | 4 | | using System.Runtime.InteropServices; |
| | | 5 | | #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER |
| | | 6 | | using System.Threading.Tasks; |
| | | 7 | | #endif |
| | | 8 | | |
| | | 9 | | namespace AmbientServices; |
| | | 10 | | |
| | | 11 | | /// <summary> |
| | | 12 | | /// An interface that can be used to mock recent CPU usage so that code branches depending on CPU utilization can be tes |
| | | 13 | | /// </summary> |
| | | 14 | | /// <remarks> |
| | | 15 | | /// <pitch>The test seam for CPU-dependent logic: register an implementation as the ambient service and <see cref="CpuMo |
| | | 16 | | /// <pledge><see cref="RecentUsage"/> returns the value to report as the most recent CPU usage, between 0.0 and 1.0; it |
| | | 17 | | /// </remarks> |
| | | 18 | | public interface IMockCpuUsage |
| | | 19 | | { |
| | | 20 | | /// <summary> |
| | | 21 | | /// Gets the value to use as the most recent CPU usage, which should be a number between 0.0 and 1.0. |
| | | 22 | | /// </summary> |
| | | 23 | | float RecentUsage { get; } |
| | | 24 | | } |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// An interface for CPU usage samplers. |
| | | 28 | | /// </summary> |
| | | 29 | | /// <remarks> |
| | | 30 | | /// <pitch>The environment-specific strategy behind <see cref="CpuMonitor"/> — one realization per way of measuring CPU |
| | | 31 | | /// <pledge><c>Sample</c> is called periodically to close a measurement window; <c>GetUsage</c> returns the utilization |
| | | 32 | | /// </remarks> |
| | | 33 | | internal interface ICpuSampler |
| | | 34 | | { |
| | | 35 | | void Sample(); |
| | | 36 | | float GetUsage(); |
| | | 37 | | float GetPendingUsage(); |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | /// <summary> |
| | | 41 | | /// A class that monitors process CPU utilization. |
| | | 42 | | /// </summary> |
| | | 43 | | /// <remarks> |
| | | 44 | | /// <pitch>Continuous, low-overhead CPU utilization for this process — a windowed average suitable for throttling decisi |
| | | 45 | | /// <pledge> |
| | | 46 | | /// <see cref="RecentUsage"/> reports the average utilization (0.0–1.0, across all processors or the container quota) ov |
| | | 47 | | /// When an ambient <see cref="IMockCpuUsage"/> is registered, <see cref="RecentUsage"/> returns its value instead of a |
| | | 48 | | /// </pledge> |
| | | 49 | | /// <plan> |
| | | 50 | | /// An <see cref="AmbientEventTimer"/> fires at the construction-time window size (default 250ms) and tells the sampler |
| | | 51 | | /// </plan> |
| | | 52 | | /// <priority> |
| | | 53 | | /// 1. What the process is actually allowed to use over what the machine has: on Linux the sampler reads the cgroup quot |
| | | 54 | | /// 2. Sampling cost independent of readers over current readings: one sample per window regardless of how many callers |
| | | 55 | | /// 3. A windowed average over an instantaneous figure: throttling on an instantaneous reading oscillates, so the report |
| | | 56 | | /// </priority> |
| | | 57 | | /// </remarks> |
| | | 58 | | public sealed class CpuMonitor : IDisposable |
| | | 59 | | #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER |
| | | 60 | | , IAsyncDisposable |
| | | 61 | | #endif |
| | | 62 | | { |
| | | 63 | | private static readonly AmbientService<IMockCpuUsage> _MockCpu = Ambient.GetService<IMockCpuUsage>(); |
| | | 64 | | private readonly AmbientEventTimer _timer = new(); |
| | | 65 | | private readonly ICpuSampler _sampler; |
| | | 66 | | |
| | | 67 | | /// <summary> |
| | | 68 | | /// Constructs a system CPU usage monitor. |
| | | 69 | | /// </summary> |
| | | 70 | | /// <param name="windowMilliseconds">The number of milliseconds between samples.</param> |
| | | 71 | | public CpuMonitor(long windowMilliseconds = 250) |
| | | 72 | | { |
| | | 73 | | _sampler = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) |
| | | 74 | | ? new LinuxContainerCpuSampler() |
| | | 75 | | : new StandardCpuSampler(); |
| | | 76 | | _timer.AutoReset = true; |
| | | 77 | | _timer.Interval = windowMilliseconds; |
| | | 78 | | _timer.Enabled = true; |
| | | 79 | | _timer.Elapsed += (s, e) => _sampler.Sample(); |
| | | 80 | | _sampler.Sample(); |
| | | 81 | | } |
| | | 82 | | /// <summary> |
| | | 83 | | /// Constructs a system CPU usage monitor. |
| | | 84 | | /// </summary> |
| | | 85 | | /// <param name="minimumWindow">A <see cref="TimeSpan"/> indicating the minimum sampling window size.</param> |
| | | 86 | | public CpuMonitor(TimeSpan minimumWindow) : this((long)minimumWindow.TotalMilliseconds) |
| | | 87 | | { |
| | | 88 | | } |
| | | 89 | | |
| | | 90 | | /// <summary> |
| | | 91 | | /// Gets the proportion of time the CPU was in use (average across all CPUs) in the previous measurement window, whi |
| | | 92 | | /// </summary> |
| | | 93 | | public float RecentUsage => _MockCpu.Local?.RecentUsage ?? _sampler.GetUsage(); |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Gets the proportion of time the CPU was in use (average across all CPUs) since the last sample was taken. |
| | | 97 | | /// </summary> |
| | | 98 | | public float PendingUsage => _sampler.GetPendingUsage(); |
| | | 99 | | |
| | | 100 | | /// <summary> |
| | | 101 | | /// Disposes of the CPU monitor. |
| | | 102 | | /// </summary> |
| | | 103 | | public void Dispose() |
| | | 104 | | { |
| | | 105 | | _timer.Enabled = false; |
| | | 106 | | _timer.Dispose(); |
| | | 107 | | } |
| | | 108 | | #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP1_0_OR_GREATER |
| | | 109 | | /// <summary> |
| | | 110 | | /// Disposes of the CPU monitor. |
| | | 111 | | /// </summary> |
| | | 112 | | /// <returns></returns> |
| | | 113 | | public ValueTask DisposeAsync() |
| | | 114 | | { |
| | | 115 | | _timer.Enabled = false; |
| | | 116 | | _timer.Dispose(); |
| | | 117 | | #if NETCOREAPP1_0_OR_GREATER |
| | | 118 | | return ValueTask.CompletedTask; |
| | | 119 | | #else |
| | | 120 | | return default; |
| | | 121 | | #endif |
| | | 122 | | } |
| | | 123 | | #endif |
| | | 124 | | } |
| | | 125 | | |
| | | 126 | | /// <summary> |
| | | 127 | | /// A class that represents a single sample of CPU usage. |
| | | 128 | | /// Two samples can be compared to see how much CPU the process used between the time the first sample was taken and the |
| | | 129 | | /// </summary> |
| | | 130 | | /// <remarks> |
| | | 131 | | /// <pitch>An immutable point-in-time pairing of wall-clock and process-CPU timestamps; utilization is only meaningful a |
| | | 132 | | /// <pledge><see cref="CpuUtilization"/> of two samples yields the process's average utilization across all processors o |
| | | 133 | | /// </remarks> |
| | | 134 | | internal readonly struct CpuSample : IEquatable<CpuSample> |
| | | 135 | | { |
| | | 136 | | /// <summary> |
| | | 137 | | /// Gets the current <see cref="Process"/>. |
| | | 138 | | /// </summary> |
| | | 139 | | /// <remarks>Note that when you want CPU usage time, this *cannot* be cached--it must be called each time.</remarks> |
| | | 140 | | /// <returns>The current <see cref="Process"/>, if available, or null if not available.</returns> |
| | | 141 | | private static Process? GetCurrentProcess() => |
| | | 142 | | #if NET5_0_OR_GREATER |
| | | 143 | | OperatingSystem.IsBrowser() ? null : |
| | | 144 | | #endif |
| | | 145 | | Process.GetCurrentProcess(); |
| | | 146 | | |
| | | 147 | | private readonly long _wallClockTicks; |
| | | 148 | | private readonly long _processTicks; |
| | | 149 | | |
| | | 150 | | private CpuSample(long wallClockTicks = 0, long processTicks = 0) |
| | | 151 | | { |
| | | 152 | | _wallClockTicks = wallClockTicks; |
| | | 153 | | _processTicks = processTicks; |
| | | 154 | | } |
| | | 155 | | /// <summary> |
| | | 156 | | /// Checks if this sample is equal to another object. |
| | | 157 | | /// </summary> |
| | | 158 | | /// <param name="other">The other CPU usage sample.</param> |
| | | 159 | | /// <returns>true if the objects are logically equal, otherwise false.</returns> |
| | | 160 | | public bool Equals(CpuSample other) |
| | | 161 | | { |
| | | 162 | | return _wallClockTicks == other._wallClockTicks && _processTicks == other._processTicks; |
| | | 163 | | } |
| | | 164 | | /// <summary> |
| | | 165 | | /// Checks if this sample is equal to another object. |
| | | 166 | | /// </summary> |
| | | 167 | | /// <param name="obj">The other object.</param> |
| | | 168 | | /// <returns>true if the objects are logically equal, otherwise false.</returns> |
| | | 169 | | public override bool Equals(object? obj) |
| | | 170 | | { |
| | | 171 | | return (obj is CpuSample other) && Equals(other); |
| | | 172 | | } |
| | | 173 | | /// <summary> |
| | | 174 | | /// Gets a hash code for this sample. |
| | | 175 | | /// </summary> |
| | | 176 | | /// <returns>The hash code.</returns> |
| | | 177 | | public override int GetHashCode() |
| | | 178 | | { |
| | | 179 | | return _wallClockTicks.GetHashCode() ^ _processTicks.GetHashCode(); |
| | | 180 | | } |
| | | 181 | | /// <summary> |
| | | 182 | | /// Checks if two samples are equal. |
| | | 183 | | /// </summary> |
| | | 184 | | /// <param name="left">The left sample.</param> |
| | | 185 | | /// <param name="right">The right sample.</param> |
| | | 186 | | /// <returns>true if the samples are logically equal, false if they are not.</returns> |
| | | 187 | | public static bool operator ==(CpuSample left, CpuSample right) |
| | | 188 | | { |
| | | 189 | | return left.Equals(right); |
| | | 190 | | } |
| | | 191 | | /// <summary> |
| | | 192 | | /// Checks if two samples are unequal. |
| | | 193 | | /// </summary> |
| | | 194 | | /// <param name="left">The left sample.</param> |
| | | 195 | | /// <param name="right">The right sample.</param> |
| | | 196 | | /// <returns>true if the samples are logically unequal, false if they are logically equal.</returns> |
| | | 197 | | public static bool operator !=(CpuSample left, CpuSample right) |
| | | 198 | | { |
| | | 199 | | return !(left == right); |
| | | 200 | | } |
| | | 201 | | /// <summary> |
| | | 202 | | /// Computes the CPU utilization between the two specified samples. |
| | | 203 | | /// </summary> |
| | | 204 | | /// <param name="first">The first sample.</param> |
| | | 205 | | /// <param name="second">The second sample.</param> |
| | | 206 | | /// <returns>The average CPU utilization (between 0.0 and 1.0) for the calling process between the time <paramref na |
| | | 207 | | public static float CpuUtilization(CpuSample first, CpuSample second) |
| | | 208 | | { |
| | | 209 | | long wallTicks = second._wallClockTicks - first._wallClockTicks; |
| | | 210 | | long cpuTicks = second._processTicks - first._processTicks; |
| | | 211 | | return Math.Min(1.0f, Math.Max(0.0f, (cpuTicks * 1.0f) / wallTicks / Environment.ProcessorCount)); |
| | | 212 | | } |
| | | 213 | | /// <summary> |
| | | 214 | | /// Samples the current CPU state for the process. |
| | | 215 | | /// </summary> |
| | | 216 | | /// <returns>A <see cref="CpuSample"/> containing the state.</returns> |
| | | 217 | | public static CpuSample GetSample() |
| | | 218 | | { |
| | | 219 | | return new(Stopwatch.GetTimestamp(), |
| | | 220 | | #if NET5_0_OR_GREATER |
| | | 221 | | OperatingSystem.IsBrowser() ? 0 : |
| | | 222 | | #endif |
| | | 223 | | GetCurrentProcess()?.TotalProcessorTime.Ticks ?? 0); |
| | | 224 | | } |
| | | 225 | | } |
| | | 226 | | |
| | | 227 | | /// <summary> |
| | | 228 | | /// The default <see cref="ICpuSampler"/>, measuring process CPU time against wall-clock time. |
| | | 229 | | /// </summary> |
| | | 230 | | /// <remarks> |
| | | 231 | | /// <pitch>The sampler for ordinary (non-cgroup-limited) environments: accurate process utilization relative to the whol |
| | | 232 | | /// <pledge><see cref="ICpuSampler"/></pledge> |
| | | 233 | | /// <plan>Keeps the last <see cref="CpuSample"/> and computes utilization as the CPU-time delta over the wall-clock delt |
| | | 234 | | /// </remarks> |
| | | 235 | | internal sealed class StandardCpuSampler : ICpuSampler |
| | | 236 | | { |
| | | 237 | | private CpuSample _lastSample; |
| | | 238 | | private float _lastUsagePercent; |
| | | 239 | | |
| | 0 | 240 | | public StandardCpuSampler() |
| | | 241 | | { |
| | 0 | 242 | | _lastSample = CpuSample.GetSample(); |
| | 0 | 243 | | } |
| | | 244 | | |
| | | 245 | | public void Sample() |
| | | 246 | | { |
| | 0 | 247 | | CpuSample newSample = CpuSample.GetSample(); |
| | 0 | 248 | | _lastUsagePercent = CpuSample.CpuUtilization(_lastSample, newSample); |
| | 0 | 249 | | _lastSample = newSample; |
| | 0 | 250 | | } |
| | | 251 | | |
| | 0 | 252 | | public float GetUsage() => _lastUsagePercent; |
| | | 253 | | |
| | | 254 | | public float GetPendingUsage() |
| | | 255 | | { |
| | 0 | 256 | | CpuSample currentSample = CpuSample.GetSample(); |
| | 0 | 257 | | return CpuSample.CpuUtilization(_lastSample, currentSample); |
| | | 258 | | } |
| | | 259 | | } |
| | | 260 | | |
| | | 261 | | /// <summary> |
| | | 262 | | /// An <see cref="ICpuSampler"/> that measures CPU usage against the Linux cgroup CPU quota, so containerized processes |
| | | 263 | | /// </summary> |
| | | 264 | | /// <remarks> |
| | | 265 | | /// <pitch>The sampler for Linux containers: a pod limited to half a CPU reads 100% when it uses its whole allowance, in |
| | | 266 | | /// <pledge><see cref="ICpuSampler"/></pledge> |
| | | 267 | | /// <pledge>When cgroup usage or quota information is unavailable (no quota set, files missing, or parse failures), usag |
| | | 268 | | /// <plan> |
| | | 269 | | /// At construction it discovers the cgroup layout once: detects v2 versus v1 (presence of <c>cgroup.controllers</c>), e |
| | | 270 | | /// Each sample reads cumulative CPU nanoseconds and computes utilization as the usage delta over (quota-fraction × elap |
| | | 271 | | /// </plan> |
| | | 272 | | /// </remarks> |
| | | 273 | | internal sealed class LinuxContainerCpuSampler : ICpuSampler |
| | | 274 | | { |
| | | 275 | | private static readonly char[] SlashCharacterArray = ['/']; |
| | | 276 | | |
| | | 277 | | private long? _lastUsage; |
| | | 278 | | private DateTime? _lastSampleTime; |
| | | 279 | | private float _lastUsagePercent; |
| | | 280 | | |
| | | 281 | | private readonly string? _cpuUsagePath; |
| | | 282 | | private readonly string? _cpuQuotaPath; |
| | | 283 | | private readonly string? _cpuPeriodPath; |
| | | 284 | | private readonly bool _isCgroupV2; |
| | | 285 | | |
| | | 286 | | /// <summary> |
| | | 287 | | /// Constructs a cgroup-based CPU sampler. For production on Linux, use the parameterless form so paths resolve to r |
| | | 288 | | /// </summary> |
| | | 289 | | /// <param name="cgroupFilesystemRoot">Optional directory that mirrors the root filesystem layout (e.g. contains <c> |
| | | 290 | | internal LinuxContainerCpuSampler(string? cgroupFilesystemRoot = null) |
| | | 291 | | { |
| | | 292 | | (_cpuUsagePath, _cpuQuotaPath, _cpuPeriodPath, _isCgroupV2, _) = DiscoverCgroupPaths(cgroupFilesystemRoot); |
| | | 293 | | } |
| | | 294 | | |
| | | 295 | | public void Sample() |
| | | 296 | | { |
| | | 297 | | long? usage = GetCgroupCpuUsage(); |
| | | 298 | | double? limit = GetCgroupCpuLimit(); |
| | | 299 | | DateTime now = AmbientClock.UtcNow; |
| | | 300 | | |
| | | 301 | | if (usage == null || limit == null) |
| | | 302 | | { |
| | | 303 | | _lastUsagePercent = 0f; |
| | | 304 | | return; |
| | | 305 | | } |
| | | 306 | | |
| | | 307 | | if (_lastUsage == null || _lastSampleTime == null) |
| | | 308 | | { |
| | | 309 | | _lastUsage = usage; |
| | | 310 | | _lastSampleTime = now; |
| | | 311 | | _lastUsagePercent = 0f; |
| | | 312 | | return; |
| | | 313 | | } |
| | | 314 | | |
| | | 315 | | long usageDelta = usage.Value - _lastUsage.Value; |
| | | 316 | | double timeDelta = (now - _lastSampleTime.Value).TotalSeconds; |
| | | 317 | | _lastUsage = usage; |
| | | 318 | | _lastSampleTime = now; |
| | | 319 | | |
| | | 320 | | if (timeDelta <= 0) |
| | | 321 | | { |
| | | 322 | | _lastUsagePercent = 0f; |
| | | 323 | | return; |
| | | 324 | | } |
| | | 325 | | |
| | | 326 | | double cpuSeconds = usageDelta / 1_000_000_000.0; |
| | | 327 | | double percent = cpuSeconds / (limit.Value * timeDelta); |
| | | 328 | | _lastUsagePercent = (float)Math.Min(Math.Max(percent, 0.0), 1.0); |
| | | 329 | | } |
| | | 330 | | |
| | | 331 | | public float GetUsage() => _lastUsagePercent; |
| | | 332 | | |
| | | 333 | | public float GetPendingUsage() |
| | | 334 | | { |
| | | 335 | | long? usage = GetCgroupCpuUsage(); |
| | | 336 | | double? limit = GetCgroupCpuLimit(); |
| | | 337 | | DateTime now = AmbientClock.UtcNow; |
| | | 338 | | |
| | | 339 | | if (usage == null || limit == null || _lastUsage == null || _lastSampleTime == null) |
| | | 340 | | return 0f; |
| | | 341 | | |
| | | 342 | | long usageDelta = usage.Value - _lastUsage.Value; |
| | | 343 | | double timeDelta = (now - _lastSampleTime.Value).TotalSeconds; |
| | | 344 | | |
| | | 345 | | if (timeDelta <= 0) |
| | | 346 | | return 0f; |
| | | 347 | | |
| | | 348 | | double cpuSeconds = usageDelta / 1_000_000_000.0; |
| | | 349 | | double percent = cpuSeconds / (limit.Value * timeDelta); |
| | | 350 | | return (float)Math.Min(Math.Max(percent, 0.0), 1.0); |
| | | 351 | | } |
| | | 352 | | |
| | | 353 | | private long? GetCgroupCpuUsage() |
| | | 354 | | { |
| | | 355 | | if (_cpuUsagePath == null) return null; |
| | | 356 | | |
| | | 357 | | long? ret = null; |
| | | 358 | | try |
| | | 359 | | { |
| | | 360 | | if (_isCgroupV2) |
| | | 361 | | { |
| | | 362 | | // cgroup v2 format: read from cpu.stat |
| | | 363 | | string[] lines = File.ReadAllLines(_cpuUsagePath); |
| | | 364 | | foreach (string line in lines) |
| | | 365 | | { |
| | | 366 | | if (line.StartsWith("usage_usec ", StringComparison.Ordinal)) |
| | | 367 | | { |
| | | 368 | | string usageStr = line.Substring("usage_usec ".Length); |
| | | 369 | | if (long.TryParse(usageStr, out long usageUsec)) |
| | | 370 | | { |
| | | 371 | | return usageUsec * 1000; // Convert to nanoseconds |
| | | 372 | | } |
| | | 373 | | } |
| | | 374 | | } |
| | | 375 | | } |
| | | 376 | | else |
| | | 377 | | { |
| | | 378 | | // cgroup v1 format: direct file read |
| | | 379 | | ret = long.Parse(File.ReadAllText(_cpuUsagePath), System.Globalization.CultureInfo.InvariantCulture); |
| | | 380 | | } |
| | | 381 | | } |
| | | 382 | | catch { } |
| | | 383 | | |
| | | 384 | | return ret; |
| | | 385 | | } |
| | | 386 | | |
| | | 387 | | private double? GetCgroupCpuLimit() |
| | | 388 | | { |
| | | 389 | | if (_cpuQuotaPath == null || _cpuPeriodPath == null) return null; |
| | | 390 | | |
| | | 391 | | double? ret = null; |
| | | 392 | | try |
| | | 393 | | { |
| | | 394 | | if (_isCgroupV2) |
| | | 395 | | { |
| | | 396 | | // cgroup v2 format: read from cpu.max |
| | | 397 | | string content = File.ReadAllText(_cpuQuotaPath).Trim(); |
| | | 398 | | string[] parts = content.Split(' '); |
| | | 399 | | if (parts.Length == 2 && long.TryParse(parts[0], out long quota) && long.TryParse(parts[1], out long per |
| | | 400 | | { |
| | | 401 | | if (quota > 0 && period > 0) ret = (double)quota / period; |
| | | 402 | | } |
| | | 403 | | } |
| | | 404 | | else |
| | | 405 | | { |
| | | 406 | | // cgroup v1 format: read from separate quota and period files |
| | | 407 | | long quota = long.Parse(File.ReadAllText(_cpuQuotaPath), System.Globalization.CultureInfo.InvariantCultu |
| | | 408 | | long period = long.Parse(File.ReadAllText(_cpuPeriodPath), System.Globalization.CultureInfo.InvariantCul |
| | | 409 | | if (quota > 0 && period > 0) ret = (double)quota / period; |
| | | 410 | | } |
| | | 411 | | } |
| | | 412 | | catch { } |
| | | 413 | | |
| | | 414 | | return ret; |
| | | 415 | | } |
| | | 416 | | |
| | | 417 | | private static string ResolvePath(string? rootPrefix, string absoluteUnixPath) |
| | | 418 | | { |
| | | 419 | | if (rootPrefix == null || string.IsNullOrEmpty(rootPrefix)) return absoluteUnixPath; |
| | | 420 | | string trimmed = absoluteUnixPath.TrimStart('/'); |
| | | 421 | | if (trimmed.Length == 0) return Path.GetFullPath(rootPrefix); |
| | | 422 | | string combined = rootPrefix; |
| | | 423 | | foreach (string segment in trimmed.Split(SlashCharacterArray, StringSplitOptions.RemoveEmptyEntries)) |
| | | 424 | | { |
| | | 425 | | combined = Path.Combine(combined, segment); |
| | | 426 | | } |
| | | 427 | | return Path.GetFullPath(combined); |
| | | 428 | | } |
| | | 429 | | |
| | | 430 | | private static (string? cpuUsagePath, string? cpuQuotaPath, string? cpuPeriodPath, bool isCgroupV2, string? containe |
| | | 431 | | { |
| | | 432 | | string? containerId = GetContainerId(cgroupFilesystemRoot); |
| | | 433 | | bool isCgroupV2 = IsCgroupV2(cgroupFilesystemRoot); |
| | | 434 | | |
| | | 435 | | if (isCgroupV2) |
| | | 436 | | { |
| | | 437 | | return DiscoverCgroupV2Paths(containerId, cgroupFilesystemRoot); |
| | | 438 | | } |
| | | 439 | | else |
| | | 440 | | { |
| | | 441 | | return DiscoverCgroupV1Paths(containerId, cgroupFilesystemRoot); |
| | | 442 | | } |
| | | 443 | | } |
| | | 444 | | |
| | | 445 | | private static string? GetContainerId(string? cgroupFilesystemRoot) |
| | | 446 | | { |
| | | 447 | | try |
| | | 448 | | { |
| | | 449 | | // Read container ID from /proc/self/cgroup |
| | | 450 | | string[] lines = File.ReadAllLines(ResolvePath(cgroupFilesystemRoot, "/proc/self/cgroup")); |
| | | 451 | | foreach (string line in lines) |
| | | 452 | | { |
| | | 453 | | // Look for docker container ID in the path |
| | | 454 | | #if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER |
| | | 455 | | if (line.Contains("docker", StringComparison.Ordinal)) |
| | | 456 | | #else |
| | | 457 | | if (line.Contains("docker")) |
| | | 458 | | #endif |
| | | 459 | | { |
| | | 460 | | string[] parts = line.Split(':'); |
| | | 461 | | if (parts.Length >= 3) |
| | | 462 | | { |
| | | 463 | | string path = parts[2]; |
| | | 464 | | // Extract container ID from path like "docker/1234567890abcdef" |
| | | 465 | | #if NETSTANDARD2_0_OR_GREATER || NETCOREAPP || NET5_0_OR_GREATER |
| | | 466 | | int dockerIndex = path.IndexOf("/docker/", StringComparison.Ordinal); |
| | | 467 | | #else |
| | | 468 | | int dockerIndex = path.IndexOf("/docker/"); |
| | | 469 | | #endif |
| | | 470 | | if (dockerIndex >= 0) |
| | | 471 | | { |
| | | 472 | | string containerPart = path.Substring(dockerIndex + 8); // Skip "/docker/" |
| | | 473 | | // Container ID is typically 64 characters, but can be shorter |
| | | 474 | | #if NETSTANDARD2_1 || NETCOREAPP || NET5_0_OR_GREATER |
| | | 475 | | int slashIndex = containerPart.IndexOf('/', StringComparison.Ordinal); |
| | | 476 | | #else |
| | | 477 | | int slashIndex = containerPart.IndexOf('/'); |
| | | 478 | | #endif |
| | | 479 | | if (slashIndex > 0) return containerPart.Substring(0, slashIndex); |
| | | 480 | | return containerPart; |
| | | 481 | | } |
| | | 482 | | } |
| | | 483 | | } |
| | | 484 | | } |
| | | 485 | | } |
| | | 486 | | catch { } |
| | | 487 | | |
| | | 488 | | return null; |
| | | 489 | | } |
| | | 490 | | |
| | | 491 | | private static bool IsCgroupV2(string? cgroupFilesystemRoot) |
| | | 492 | | { |
| | | 493 | | // Check if cgroup v2 is mounted |
| | | 494 | | return File.Exists(ResolvePath(cgroupFilesystemRoot, "/sys/fs/cgroup/cgroup.controllers")); |
| | | 495 | | } |
| | | 496 | | |
| | | 497 | | private static (string? cpuUsagePath, string? cpuQuotaPath, string? cpuPeriodPath, bool isCgroupV2, string? containe |
| | | 498 | | { |
| | | 499 | | string? cpuUsagePath = null; |
| | | 500 | | string? cpuQuotaPath = null; |
| | | 501 | | string? cpuPeriodPath = null; |
| | | 502 | | |
| | | 503 | | // Try different possible paths for cgroup v2 |
| | | 504 | | string[] possibleBasePaths = { |
| | | 505 | | "/sys/fs/cgroup", |
| | | 506 | | $"/sys/fs/cgroup/docker/{containerId}", |
| | | 507 | | $"/sys/fs/cgroup/system.slice/docker-{containerId}.scope" |
| | | 508 | | }; |
| | | 509 | | |
| | | 510 | | foreach (string basePath in possibleBasePaths) |
| | | 511 | | { |
| | | 512 | | string resolvedBase = ResolvePath(cgroupFilesystemRoot, basePath); |
| | | 513 | | if (Directory.Exists(resolvedBase)) |
| | | 514 | | { |
| | | 515 | | string cpuStatPath = Path.Combine(resolvedBase, "cpu.stat"); |
| | | 516 | | string cpuMaxPath = Path.Combine(resolvedBase, "cpu.max"); |
| | | 517 | | |
| | | 518 | | if (File.Exists(cpuStatPath) && File.Exists(cpuMaxPath)) |
| | | 519 | | { |
| | | 520 | | cpuUsagePath = cpuStatPath; |
| | | 521 | | cpuQuotaPath = cpuMaxPath; |
| | | 522 | | cpuPeriodPath = cpuMaxPath; // Same file for v2 |
| | | 523 | | break; |
| | | 524 | | } |
| | | 525 | | } |
| | | 526 | | } |
| | | 527 | | |
| | | 528 | | return (cpuUsagePath, cpuQuotaPath, cpuPeriodPath, true, containerId); |
| | | 529 | | } |
| | | 530 | | |
| | | 531 | | private static (string? cpuUsagePath, string? cpuQuotaPath, string? cpuPeriodPath, bool isCgroupV2, string? containe |
| | | 532 | | { |
| | | 533 | | string? cpuUsagePath = null; |
| | | 534 | | string? cpuQuotaPath = null; |
| | | 535 | | string? cpuPeriodPath = null; |
| | | 536 | | |
| | | 537 | | // Try different possible paths for cgroup v1 |
| | | 538 | | string[] possibleBasePaths = { |
| | | 539 | | "/sys/fs/cgroup/cpuacct", |
| | | 540 | | $"/sys/fs/cgroup/cpuacct/docker/{containerId}", |
| | | 541 | | $"/sys/fs/cgroup/cpuacct/system.slice/docker-{containerId}.scope" |
| | | 542 | | }; |
| | | 543 | | |
| | | 544 | | string[] possibleCpuBasePaths = { |
| | | 545 | | "/sys/fs/cgroup/cpu", |
| | | 546 | | $"/sys/fs/cgroup/cpu/docker/{containerId}", |
| | | 547 | | $"/sys/fs/cgroup/cpu/system.slice/docker-{containerId}.scope" |
| | | 548 | | }; |
| | | 549 | | |
| | | 550 | | // Find CPU usage path |
| | | 551 | | foreach (string basePath in possibleBasePaths) |
| | | 552 | | { |
| | | 553 | | string resolvedBase = ResolvePath(cgroupFilesystemRoot, basePath); |
| | | 554 | | string usagePath = Path.Combine(resolvedBase, "cpuacct.usage"); |
| | | 555 | | if (File.Exists(usagePath)) |
| | | 556 | | { |
| | | 557 | | cpuUsagePath = usagePath; |
| | | 558 | | break; |
| | | 559 | | } |
| | | 560 | | } |
| | | 561 | | |
| | | 562 | | // Find CPU quota and period paths |
| | | 563 | | foreach (string basePath in possibleCpuBasePaths) |
| | | 564 | | { |
| | | 565 | | string resolvedBase = ResolvePath(cgroupFilesystemRoot, basePath); |
| | | 566 | | string quotaPath = Path.Combine(resolvedBase, "cpu.cfs_quota_us"); |
| | | 567 | | string periodPath = Path.Combine(resolvedBase, "cpu.cfs_period_us"); |
| | | 568 | | |
| | | 569 | | if (File.Exists(quotaPath) && File.Exists(periodPath)) |
| | | 570 | | { |
| | | 571 | | cpuQuotaPath = quotaPath; |
| | | 572 | | cpuPeriodPath = periodPath; |
| | | 573 | | break; |
| | | 574 | | } |
| | | 575 | | } |
| | | 576 | | |
| | | 577 | | return (cpuUsagePath, cpuQuotaPath, cpuPeriodPath, false, containerId); |
| | | 578 | | } |
| | | 579 | | } |