Monday, December 15, 2025

Introducing GpTimestamp

In my programmer's life I frequently deal with all kinds of timestamps. As the code is relatively, ahem, "mature" those timestamps come from all kinds of sources. There are 64-bit ticks (GetTickCount64), multimedia time (timeGetTime), performance counters (QueryPerformanceCounter), performance counters in disguise (TStopwatch), and there is also a TDateTime (NowUTC) mixed in from time to time.

All this mess would not be that bad had the timestamps been only used locally. If you sample a timestamp and then a few lines later check whether so-and-so time has expired, all is well. You will always know what the source of the timestamp was and what to compare it to.

Problems occur when the timestamps are grabbed on one side of the system and consumed on the other side. Let's say that the code receives a block of data over some internal communication mechanism. A part of that data is a timestamp representing a creation time of that data block. You have to check how much time has elapsed since then. How do you do that?

Well, firstly you have to know in what units this timestamp is stored. Is it a floating point number (TDateTime) or a 64-bit integer? In the latter case, does it store a number of milliseconds, 'ticks' (whatever that is) or something else?

Secondly, you have to get "current timestamp" from the correct time source so you can do the calculation. So you have to know what the source was. And if you're not sure, you'll have to dig through the code to find the point where the original timestamp was created.

The former problem is somewhat solvable by using descriptive variable/field names. For example, if a variable stores a timestamp in milliseconds, I always use a suffix _ms. In the example above I would use a field DataCreationTime_ms or something similar.

The latter problem, however, is hard. You could use suffixes (DataCreationTime_Tick64_ms) but that gets ugly pretty fast. You could also document the time source (and you should!) but that again stops you when you just want to write some code.

Even worse, both solutions only work if you take care when coding. Delphi won't stop you from subtracting TStopwatch.GetTimeStamp from a value that is storing a result of timeGetTime, although the result won't make much sense.

Clearly, I needed something better, so I wrote* the GpTimestamp unit which tries to solve this mess. There is also documentation and unit tests, if you want to jump in now.

(* When I say "I" I actually mean Claude. I was just an idea-man on this project. I'll say more about that in the following post...)

The core of the solution is record type TGpTimestamp. It uses only 16 bytes to store two bits of information:

  • Timestamp
  • Timestamp source

Internally, timestamps are stored in nanoseconds. That should be enough if you are measuring intervals shorter than 292 years ;) As this type is not meant to be used for permanent timestamps (stored externally - in registry, files, databases etc) that should definitely be enough. TDateTime-based timestamps are stored as a number of nanoseconds since 2025-12-12T00:00:00. Again, that should be enough for anyone. (If you are still using this code in the year 2317 then something has horribly gone wrong. I'm sorry.)

Side note: The whole usefulness of having a TDateTime time source is still under consideration. It may be removed in the future. Or not.

So, back to the time sources. TGpTimestamp can grab timestamps from different sources:

  • TStopwatch (cross-platform)
  • TDateTime (cross-platform, uses UTC time)
  • GetTickCount64 (Windows)
  • DSiTimeGetTime64 (Windows, requires DSiWin32)
  • QueryPerformanceCounter (Windows)

To initialize a timestamp, use a FromXXX class function (with or without a parameter). For example, you can do:

var ts1 := TGpTimestamp.FromQueryPerformanceCounter;
var ts2 := _TS_.FromQueryPerformanceCounter(ts1);

This will create two equal timestamps.

Side note: _TS_ is just an alias for TGpTimestamp.

You can compare two timestamps (if ts1 >= ts2), but only if they are based on the same source. Otherwise the code will throw an exception.

In addition to the time sources listed above, TGpTimestamp also supports 'duration' timestamps. They can be created by subtracting two 'normal' timestamps (ts1 - ts2 will return a TGpTimestamp record with a value 0 and time source 'duration') or by calling functions Hours, Minutes, Seconds, Milliseconds, Microseconds, or Nanoseconds. You can also use a ToXXX set of functions to convert an internal value to a specified unit (ToSeconds, ToMilliseconds etc).

You can add a duration to a timestamp (ts1 + _TS_.Microseconds(500)) or subtract it from a timestamp, but you cannot subtract a timestamp from a duration (as that makes no sense).

There are also functions that check how much time has elapsed since the timestamp was created (Elapsed) and whether a specified duration has elapsed (HasElapsed(duration)). Current timestamp for the compatible time source is retrieved automatically. HasElapsed returns True if timestamp has not been initialized, which helps simplify some programming patterns. See more in the documentation.

There are ToString (for displaying the value), ToDebugString (for logs) and read/write AsString property (for serialization). Plus some other helper functions but you can find more about them in the code. Or in the documentation.

Conclusion

I'm not sure how useful TGpTimestamp is for a common Delphi programmer, but it surely is useful for me. With the hope that someone else finds this useful, I put it on GitHub, along with many other (more or less useful) units.

If you find it useful, please let me know. If you have any other ideas how this unit could be extended, contact me.

I find the whole concept of a wrapper that checks some metadata before applying calculations on a value quite interesting. It could be applied to other fields too, not just time. For example, one could create a type (or, most probably, a set of types) that handles units of measure and prevents a mess-up when - for example - one multiplies inches by furlongs and expects to get square micrometres.



2 comments:

  1. I'm a huge fan of boxed types for things like this. I never found a good solution in old school Delphi so I used single-member classes. In C++ I have a template that does the work (and has comparison operators etc). My C++ code is full of those, not least database ID types (passing a TUserID instead of a TPaymentID is a compile error!)

    That at least stops you assigning nanoseconds to floating point days.

    ReplyDelete
  2. Mentioned TGpTimestamp is a good example of boxed type. This is "smart" record with methods and class operators.
    TOmniValue in Omni Thread Library is another good example.

    ReplyDelete