Description
Description
It looks like, that the way that System.Diagnostics.Process.StartTime
is calculated is not stable. That is, if you get the start time multiple times, you get different values - even if just in the millisecond range.
This is a problem, not only for precision in general, but also for applications that use a process' start time together with its process ID to uniquely identify it. Also, Windows doesn't have this problem, making porting and compatibility tricky. Finally, the current behavior does not allow other code to reproduce the exact same start time that the CLR calculates (something which, again, is possible on Windows). There might also be issues comparing this start time with the one reported by other (native) tools. Again, which might cause "identity problems".
Reproduction Steps
Consider the following Program:
using System.Diagnostics;
using System;
public static class Program
{
public static void Main(string[] args)
{
using var process = Process.GetProcessById(Int32.Parse(args[0]));
Console.WriteLine(process.StartTime.ToString("O"));
}
}
On a Linux system invoke it multiple times (e.g. for the init process, PID 1)
$ dotnet bin/Debug/net8.0/starttime 1
Expected behavior
Multiple invocations produce the exact same start time for the target process.
Actual behavior
Multiple invocations produce different start times for the target process (actual output):
$ dotnet ./bin/Debug/net8.0/starttime.dll 1
2024-10-17T03:12:50.9611154+02:00
$ dotnet ./bin/Debug/net8.0/starttime.dll 1
2024-10-17T03:12:50.9520283+02:00
$ dotnet ./bin/Debug/net8.0/starttime.dll 1
2024-10-17T03:12:50.9505063+02:00
Regression?
No response
Known Workarounds
Not a workaround, but it kind of "works" if you only ever use such timestamps from within the same process, because inside Process.Linux.cs, the boot time is cached (in s_bootTimeTicks).
Configuration
- .NET 8.0
- Ubuntu 23.04, x64
- WSL 2
Other information
It looks like it is actually an issue in the implementation of the native SystemNative_GetBootTimeTicks
function in pal_time.c
. It uses CLOCK_REALTIME_COARSE
to calculate part of the time stamp and that returns unstable (or "less precise") values.
If I understand the rest of the code to get Process.StartTime
correctly, all other values (like starttime
from the process' procfs-stat file) are basically constants. So the observed behavior should be related to the native code.
Consider the following "low-level" repro (code copied from pal_time.c
):
/* build: clang foo.cpp */
#include<time.h>
#include<stdint.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
enum
{
MicroSecondsToNanoSeconds = 1000, // 10^3
SecondsToNanoSeconds = 1000000000, // 10^9
SecondsToTicks = 10000000, // 10^7
TicksToNanoSeconds = 100, // 10^2
};
const int64_t UnixEpochTicks = 621355968000000000;
int main(int argc, char** argv) {
int64_t previous = 0;
for (int i = 0; i < 10; i++) {
struct timespec ts;
int result = clock_gettime(CLOCK_BOOTTIME, &ts);
assert(result == 0);
int64_t sinceBootTicks = ((int64_t)ts.tv_sec * SecondsToTicks) + (ts.tv_nsec / TicksToNanoSeconds);
if (strcmp(argv[1], "--mono") == 0) {
result = clock_gettime(CLOCK_MONOTONIC, &ts);
} else {
result = clock_gettime(CLOCK_REALTIME_COARSE, &ts);
}
assert(result == 0);
int64_t sinceEpochTicks = ((int64_t)ts.tv_sec * SecondsToTicks) + (ts.tv_nsec / TicksToNanoSeconds);
int64_t bootTime = UnixEpochTicks + sinceEpochTicks - sinceBootTicks;
if (i > 0) {
printf("sinceBootTicks: %ld, sinceEpochTicks: %ld, bootTime: %ld, delta: %ld\n",
sinceBootTicks, sinceEpochTicks, bootTime, bootTime - previous);
} else {
printf("sinceBootTicks: %ld, sinceEpochTicks: %ld, bootTime: %ld\n",
sinceBootTicks, sinceEpochTicks, bootTime);
}
previous = bootTime;
sleep(1);
}
return 0;
}
If you compile and invoke the resulting binary with --mono
it will use CLOCK_REALTIME_COARSE
rather than CLOCK_REALTIME_COARSE
and will produce stable results:
./a.out --coarse
sinceBootTicks: 230188979998, sinceEpochTicks: 17291505892061801, bootTime: 638647243703081803
sinceBootTicks: 230199052191, sinceEpochTicks: 17291505902161801, bootTime: 638647243703109610, delta: 27807
sinceBootTicks: 230209124116, sinceEpochTicks: 17291505912261800, bootTime: 638647243703137684, delta: 28074
sinceBootTicks: 230219196751, sinceEpochTicks: 17291505922161794, bootTime: 638647243702965043, delta: -172641
sinceBootTicks: 230229269369, sinceEpochTicks: 17291505932361787, bootTime: 638647243703092418, delta: 127375
sinceBootTicks: 230239345412, sinceEpochTicks: 17291505942461781, bootTime: 638647243703116369, delta: 23951
sinceBootTicks: 230249416194, sinceEpochTicks: 17291505952461774, bootTime: 638647243703045580, delta: -70789
Compare with
./a.out --mono
sinceBootTicks: 229996721520, sinceEpochTicks: 229996721534, bootTime: 621355968000000014
sinceBootTicks: 230006794304, sinceEpochTicks: 230006794308, bootTime: 621355968000000004, delta: -10
sinceBootTicks: 230016866885, sinceEpochTicks: 230016866889, bootTime: 621355968000000004, delta: 0
sinceBootTicks: 230026938539, sinceEpochTicks: 230026938543, bootTime: 621355968000000004, delta: 0
sinceBootTicks: 230037009285, sinceEpochTicks: 230037009294, bootTime: 621355968000000009, delta: 5
sinceBootTicks: 230047080913, sinceEpochTicks: 230047080917, bootTime: 621355968000000004, delta: -5
sinceBootTicks: 230057096215, sinceEpochTicks: 230057096219, bootTime: 621355968000000004, delta: 0
As you can see, the difference (in ticks) between the individual calculations is much smaller.
Albeit still not 100% identical. So I'm not sure if that would really be a fix - given nothing
else really requires CLOCK_REALTIME_COARSE
here.