Greetings, fellow geeks! I hope to occasionally find time to write blog posts that are hopefully meaningful to others. Like many other vCampus customers, I consider myself to be working deep in the trenches where we oftentimes must supplement the OSIsoft offerings with some of our own patchwork code to provide a more comprehensive solution, or even just a simple workaround to our own customers. It’s in the spirit of finding such solutions that I would dedicate my own blogs.
My inaugural blog will cover the topic of time. Or more so, the resolution of time as it may be on a client front-end and how that may differ from what might be coming from a backend PI server. This topic may be particularly relevant to anyone needing to work with sub-second data.
A couple years back I had a gnarly 3-day bug hunt trying to find out why the AFTime value coming back from a PI Point didn’t exactly match the start time from a GetValues call. Some info about my environment: I was testing on my 64-bit Windows 7 desktop against an AFAttribute using the PI Point data reference. The PI Point was interpolated, i.e. not stepped. I would set the end of the time range to AFTime.Now, and set the start to 8 hours before the end. And for reasons that shall remain undisclosed, I was checking that the passed start time (now minus 8 hours) was exactly equal to the first returned timestamp for the PI Point over that time range.
I was quite surprised to find out that they weren’t exactly the same – when I thought they should be. In fact, it took me over 2.5 days to find that much out. The fact that they weren’t equal was slightly obscured as I initially dumped out the UtcSeconds for the timestamps. What I first discovered was something like:
Hey, those look equal to me. How about you? But something still wasn’t right, so I had to dig deeper. I decided to chop off the whole number portion of UtcSeconds and focus on the decimal portion.
Ahhhh, now my interest got peaked. This times are indeed different staring in the 6th decimal place. In this example, I show a difference of:
Delta 2.86102294921875E-06 of one second
I began to ponder just how could that be? At first I wondered if that’s attributable to the difference between AFTime versus PITime. But, no, it’s wasn’t that. It soon dawned on me that the problem was my development environment had a high resolution timer for when AFTime.Now is invoked. And while a sub-second PI server can serve up sub-second data, that sub-second resolution will be in intervals of 65536 sub-second slices per second. That was my problem: AFTime.Now had a finer sub-second resolution and wasn’t guaranteed to be on a ‘whole’ PI sub-second slice (if I have explained that adequately) and therefore differed from the returned PI server time’s UtcSeconds starting in the 6th decimal place. I did mention gnarly, right?
For those of us who can live with whole second data, OSIsoft has 2 methods worth mentioning. Ever since AF 2.3, the AFSDK offers both the AFTime.NowInWholeSeconds property, compliments of yours truly. Not that I wrote it, but I did request it. And there is also the AFTime.TruncateToWholeSeconds method. These hopefully should satisfy many of you, except perhaps those in Electrical or Power industries where sub-second timings are critical. For those, I offer a bit of code to compliment the TruncateToWholeSeconds method.
I call the extension method TruncateToPISubseconds where again I define a “PI server sub-second” as 1/65536th of a second.
C# Extension Method
public static class SomeTimeExtensions
public static AFTime TruncateToPISubseconds(this AFTime inputTime)
AFTime outputTime = new AFTime(inputTime);
double Seconds = outputTime.UtcSeconds;
double WholeSeconds = System.Math.Truncate(Seconds);
if (Seconds != WholeSeconds)
const int SlicesPerSecond = 65536;
double Slices = (Seconds - WholeSeconds) * SlicesPerSecond;
double WholeSlices = System.Math.Truncate(Slices);
if (Slices != WholeSlices)
double Subseconds = WholeSlices / SlicesPerSecond;
outputTime = new AFTime(WholeSeconds + Subseconds);
VB.NET Extension Method
Public Module SomeTimeExtensions
Public Function TruncateToPISubseconds(inputTime As AFTime) As AFTime
Dim outputTime As New AFTime(inputTime)
Dim Seconds As Double = outputTime.UtcSeconds
Dim WholeSeconds As Double = System.Math.Truncate(Seconds)
If Seconds <> WholeSeconds Then
Const SlicesPerSecond As Integer = 65536
Dim Slices As Double = (Seconds - WholeSeconds) * SlicesPerSecond
Dim WholeSlices As Double = System.Math.Truncate(Slices)
If Slices <> WholeSlices Then
Dim Subseconds As Double = WholeSlices / SlicesPerSecond
outputTime = New AFTime(WholeSeconds + Subseconds)
In summary, there are 2 bits of advice that I give to every developer new to time series data:
First, is to never just use AFTime.Now as is, and especially not in a loop. Since Now changes with every invocation, you run the risk that data returned is not at the same event horizon, even if they are mere milliseconds apart. Rather you should freeze AFTime.Now into a variable, and use that variable’s value for the duration of your procedure.
And second, you should consider truncating AFTime.Now to an acceptable resolution as required by your application. If your PI data is saved to whole seconds, then use one of the AFSDK methods to truncate Now to whole seconds. If you are using sub-second PI, that is 1/65536th of a second data, you may find the above extension methods useful. If you work with whole minutes, there’s nothing stopping you from writing your own extension method!