diff --git a/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs b/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs index 36cdea9e1bd9a4..40423ae3d2083b 100644 --- a/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs +++ b/src/libraries/Common/src/Interop/OSX/Interop.Libraries.cs @@ -11,6 +11,7 @@ internal static partial class Libraries internal const string libobjc = "/usr/lib/libobjc.dylib"; internal const string libproc = "/usr/lib/libproc.dylib"; internal const string Odbc32 = "libodbc.2.dylib"; + internal const string libSystem = "libSystem.dylib"; internal const string OpenLdap = "libldap.dylib"; internal const string SystemConfigurationLibrary = "/System/Library/Frameworks/SystemConfiguration.framework/SystemConfiguration"; internal const string AppleCryptoNative = "libSystem.Security.Cryptography.Native.Apple"; diff --git a/src/libraries/Common/src/Interop/OSX/Interop.libSystem.cs b/src/libraries/Common/src/Interop/OSX/Interop.libSystem.cs new file mode 100644 index 00000000000000..e8662da28e709d --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/Interop.libSystem.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class libSystem + { + [LibraryImport(Interop.Libraries.libSystem)] + public static unsafe partial int mach_timebase_info(mach_timebase_info_data_t* info); + public struct mach_timebase_info_data_t + { + public uint numer; + public uint denom; + } + } +} diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj index e82e069ffd3ed5..ddd518ff333e09 100644 --- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj +++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj @@ -317,6 +317,8 @@ Link="Common\Interop\OSX\Interop.libproc.cs" /> + diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs index da44fc1ed3426f..07f55780d82de9 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.OSX.cs @@ -10,6 +10,7 @@ namespace System.Diagnostics public partial class Process { private const int NanosecondsTo100NanosecondsFactor = 100; + private static volatile uint s_timeBase_numer, s_timeBase_denom; private const int MicrosecondsToSecondsFactor = 1_000_000; @@ -23,7 +24,7 @@ public TimeSpan PrivilegedProcessorTime { EnsureState(State.HaveNonExitedId); Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId); - return new TimeSpan(Convert.ToInt64(info.ri_system_time / NanosecondsTo100NanosecondsFactor)); + return MapTime(info.ri_system_time); } } @@ -65,7 +66,7 @@ public TimeSpan TotalProcessorTime { EnsureState(State.HaveNonExitedId); Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId); - return new TimeSpan(Convert.ToInt64((info.ri_system_time + info.ri_user_time) / NanosecondsTo100NanosecondsFactor)); + return MapTime(info.ri_system_time + info.ri_user_time); } } @@ -82,7 +83,7 @@ public TimeSpan UserProcessorTime { EnsureState(State.HaveNonExitedId); Interop.libproc.rusage_info_v3 info = Interop.libproc.proc_pid_rusage(_processId); - return new TimeSpan(Convert.ToInt64(info.ri_user_time / NanosecondsTo100NanosecondsFactor)); + return MapTime(info.ri_user_time); } } @@ -109,5 +110,35 @@ private static Interop.libproc.rusage_info_v3 GetCurrentProcessRUsage() { return Interop.libproc.proc_pid_rusage(Environment.ProcessId); } + + private static TimeSpan MapTime(ulong sysTime) + { + uint denom = s_timeBase_denom; + if (denom == default) + { + Interop.libSystem.mach_timebase_info_data_t timeBase = GetTimeBase(); + s_timeBase_numer = timeBase.numer; + s_timeBase_denom = denom = timeBase.denom; + } + uint numer = s_timeBase_numer; + + // By dividing by NanosecondsTo100NanosecondsFactor first, we lose some precision, but increase the range + // where no overflow will happen. + return new TimeSpan(Convert.ToInt64(sysTime / NanosecondsTo100NanosecondsFactor * numer / denom)); + } + + private static unsafe Interop.libSystem.mach_timebase_info_data_t GetTimeBase() + { + Interop.libSystem.mach_timebase_info_data_t timeBase = default; + var returnCode = Interop.libSystem.mach_timebase_info(&timeBase); + Debug.Assert(returnCode == 0, $"Non-zero exit code from mach_timebase_info: {returnCode}"); + if (returnCode != 0) + { + // Fallback: let's assume that the time values are in nanoseconds, + // i.e. the time base is 1/1. + timeBase.numer = timeBase.denom = 1; + } + return timeBase; + } } } diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs index 2085fddbb5d202..79ad7ec01f0a13 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessTests.Unix.cs @@ -851,6 +851,39 @@ static void ExecuteChildProcess(string filename, string flags) } } + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public unsafe void TestTotalProcessorTimeMacOs() + { + var rUsage = Interop.libproc.proc_pid_rusage(Environment.ProcessId); + var timeBase = new Interop.libSystem.mach_timebase_info_data_t(); + Interop.libSystem.mach_timebase_info(&timeBase); + + var nativeUserUs = rUsage.ri_user_time / 1000 * timeBase.numer / timeBase.denom; + var nativeSystemUs = rUsage.ri_system_time / 1000 * timeBase.numer / timeBase.denom; + var nativeTotalUs = nativeSystemUs + nativeUserUs; + + var nativeUserTime = TimeSpan.FromMicroseconds(nativeUserUs); + var nativeSystemTime = TimeSpan.FromMicroseconds(nativeSystemUs); + var nativeTotalTime = TimeSpan.FromMicroseconds(nativeTotalUs); + + var process = Process.GetCurrentProcess(); + var managedUserTime = process.UserProcessorTime; + var managedSystemTime = process.PrivilegedProcessorTime; + var managedTotalTime = process.TotalProcessorTime; + + AssertTime(managedUserTime, nativeUserTime, "user"); + AssertTime(managedSystemTime, nativeSystemTime, "system"); + AssertTime(managedTotalTime, nativeTotalTime, "total"); + + void AssertTime(TimeSpan managed, TimeSpan native, string label) + { + Assert.True( + managed >= native, + $"Time '{label}' returned by managed API ({managed}) should be greated or equal to the time returned by native API ({native})."); + } + } + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(true)] [InlineData(false)] diff --git a/src/libraries/System.Diagnostics.Process/tests/Resources/Strings.resx b/src/libraries/System.Diagnostics.Process/tests/Resources/Strings.resx index 4fd9192b919e10..d443842f56cbc6 100644 --- a/src/libraries/System.Diagnostics.Process/tests/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.Process/tests/Resources/Strings.resx @@ -120,4 +120,10 @@ The argv[0] argument cannot include a double quote. + + Could not get all running Process IDs. + + + Failed to set or retrieve rusage information. See the error code for OS-specific error information. + \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj index c3f01adc3bc673..889e34344048ac 100644 --- a/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj +++ b/src/libraries/System.Diagnostics.Process/tests/System.Diagnostics.Process.Tests.csproj @@ -63,6 +63,12 @@ + + +