Skip to content

On Linux, System.Diagnostics.Process.StartTime is not stable #108959

Open
@cklutz

Description

@cklutz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Diagnostics.Processhelp wanted[up-for-grabs] Good issue for external contributorsneeds-further-triageIssue has been initially triaged, but needs deeper consideration or reconsideration

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions