Skip to content

Support microsecond precision in realTime and realTimeInstant on the JVM#2416

Merged
djspiewak merged 3 commits into
typelevel:series/3.xfrom
brendanmaguire:support-microsecond-precision-on-jvm
Mar 20, 2022
Merged

Support microsecond precision in realTime and realTimeInstant on the JVM#2416
djspiewak merged 3 commits into
typelevel:series/3.xfrom
brendanmaguire:support-microsecond-precision-on-jvm

Conversation

@brendanmaguire

Copy link
Copy Markdown
Contributor
  • Use java.time.Instant to provide microsecond precision for nowMicros
  • Use nowMicros instead of nowMillis to determine the real time

This is a follow up to #1461

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

I guess there needs to be a change made in SyncIO also but I'm not sure what that is. java.time.Instant won't work in code shared with Javascript.

@vasilmkd vasilmkd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for opening this PR!

I'm not against this change, but we'll need to think it through. Waiting for comments from other maintainers.

Comment thread core/shared/src/main/scala/cats/effect/IOFiber.scala

def nowMillis(): Long

def nowMicros(): Long

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need a default implementation similar to the one for Scala.js, otherwise this is a binary incompatible change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for my understanding would you mind pointing to the default implementation for Scala.js?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You wrote it yourself, nowMillis() * 1000. 😁

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you mean now 😅 It needs a default implementation to ensure backwards compatibility with existing implementations. Thanks

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, and because of this we end up with a method that can only make a best-effort to provide microsecond precision with no real guarantee.

self.applicative.map(self.realTime)(d => Instant.ofEpochMilli(d.toMillis))
}
def realTimeInstant: F[Instant] =
self.applicative.map(self.realTime)(d => Instant.EPOCH.plusNanos(d.toNanos))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nanos since the epoch, seems like a tall ask, but I'm not really familiar with time APIs, so I might be wrong.

@armanbilge

armanbilge commented Oct 16, 2021

Copy link
Copy Markdown
Member

For JS side of things, just want to point out this:
https://developer.mozilla.org/en-US/docs/Web/API/Performance/now

Unlike other timing data available to JavaScript (for example Date.now), the timestamps returned by performance.now() are not limited to one-millisecond resolution. Instead, they represent times as floating-point numbers with up to microsecond precision.

Besides @vasilmkd's comments above, here is my concern/confusion: IIUC we can't change the semantics of Clock#realTime (it must be System.currentTimeMillis per the docs). So if we are adding a method e.g. realTimeMicros to Clock typeclass, it needs to have a default implementation given in terms of realTime, and thus can only make a best effort promise for microsecond precision, since it will rely on implementors to override and provide this. Furthermore, implementors are not just IO and SyncIO, but any monad transformer (e.g., the Clock implementations for Resource, OptionT, etc.). So not only would there be no theoretical guarantee of microsecond precision, in practice it would possibly be quite easy to encounter this problem. IMHO, adding a method with such a weak contract is ... not a particularly helpful API.

It seems to me a better way to accomplish this change would be to introduce a new typeclass e.g. PreciseClock that can make a guarantee. I think we could even define this in std and provide a default implementation in terms of Sync.

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

Thanks for the comments @vasilmkd & @armanbilge

My intention with this PR was to port the changes that were made in CE2 (#1461). As it seems this change will require more planning I'm happy if you would like to close this PR and start a discussion in the issues?

@brendanmaguire
brendanmaguire force-pushed the support-microsecond-precision-on-jvm branch 2 times, most recently from dbd1de7 to 38b2f53 Compare October 17, 2021 10:39
@djspiewak

djspiewak commented Oct 17, 2021

Copy link
Copy Markdown
Member

If you dig through the sources for Instant and Clock, you'll note that the value you're returning here is actually based on currentTimeMillis and nanoTime (indirectly). The OS doesn't provide a nanosecond-precision function for obtaining system time (nor should it, given the cost of that syscall), and so the only way to get nanosecond precision "real" time is to compute the offset between nanoTime and currentTimeMillis, then use that adjustment paired with nanoTime to project back into real time. You can periodically update the offset to catch system clock adjustments, but this forms the basis of a higher-precision internal clock at the cost of delays on reflecting system time changes. As a bonus, it's also about an order of magnitude faster to do this.

We should provide this functionality in Clock. :-) Http4s already uses something like it internally. But we don't actually need to introduce new primitives in this fashion, since the functionality in the PR is actually implementable in terms of monotonic and realTime, just the same way that java.time does it.

Edit: @brendanmaguire If you're interested in pushing on this, I would recommend starting from the following signatures (which can be added to Clock):

// should return the delta between nanoTime and currentTimeMillis
// held out as a separate function so implementations can override it with a more accurate/faster version
def realNanoOffset: F[FiniteDuration] = ???

def fastRealTime: F[F[FiniteDuration]] = ???

Bikeshed the names. I think you'll need to add the latter to Temporal rather than Clock because I believe you need Concurrent to implement it. The former should be overridden in IO (at least on the JVM) to avoid auto-ceding issues. If the latter is implemented correctly, it will provide a nanosecond precision real time clock (as precise as Instant), and much better performance than calling realTime.

@brendanmaguire

brendanmaguire commented Oct 18, 2021

Copy link
Copy Markdown
Contributor Author

@djspiewak Thanks for the detailed comment 🙂

I could be misunderstanding here but from looking at the Instant.now() implementation it uses jdk.internal.misc.VM.getNanoTimeAdjustment to determine the nanosecond offset from System.currentTimeMillis. So it seems to me we cannot use monotonic and realTime to provide the same functionality.

Edit: Ah, unless your suggestion is to do this natively in both JS and the JVM?

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

Interested to hear your thoughts on the above @djspiewak . I'm happy to take on this task but I'm not sure what is required at the moment.

@vasilmkd

vasilmkd commented Oct 19, 2021

Copy link
Copy Markdown
Member

I could be misunderstanding here but from looking at the Instant.now() implementation it uses jdk.internal.misc.VM.getNanoTimeAdjustment to determine the nanosecond offset from System.currentTimeMillis. So it seems to me we cannot use monotonic and realTime to provide the same functionality.

You might be right about this. We need to investigate a bit more.

@armanbilge

Copy link
Copy Markdown
Member

This came up again in typelevel/natchez#427 (comment), again to do with tracing. Any ideas to unblock this? What about my PreciseClock idea in std?

@armanbilge

armanbilge commented Nov 10, 2021

Copy link
Copy Markdown
Member

To aid discussion, I'm pretty sure this is what @djspiewak was proposing above in #2416 (comment). Unless I completely misunderstood something, actually this can be implemented etnirely with Clock (we don't need Temporal). Edit: nevermind, I think I missed the point of Temporal, it's to implement an auto-refresh strategy, see below.

  /**
   * Calculates the offset between the real time and the monotonic nanotime, i.e.:
   * realNanoOffset = realTime - monotonic.
   */
  def realNanoOffset: F[FiniteDuration] =
    applicative.map2(realTime, monotonic)(_ - _)

  /**
   * A representation of the current system time with up to nanosecond accuracy.
   */
  def fastRealTime: F[F[FiniteDuration]] =
    applicative.map(realNanoOffset)(offset => applicative.map(monotonic)(_ + offset))

Personally, I'm not crazy about this approach (tl;dr I think it's annoying to implement and annoying to use), and I just proposed a different strategy in #2529.

Edit: just to expand my tl;dr a bit (all IMHO, and of course open to be convinced otherwise):

  1. Why is it annoying to implement? Because event though JDK9+ provides a microsecond-precision clock, as @brendanmaguire points out getNanoTimeAdjustment is an internal VM method. So it's an implementation in terms of something we don't generally have access to, to provide a default implementation for something we do generally have access to.
  2. Why is it annoying to use? As a user IDK what to make of F[F[FiniteDuration]]. I might just .flatten it (this is mathematically equivalent to just using realTime directly) or maybe cache it once and use it forever (but IIUC this is liable to drift or otherwise miss clock adjustments). Developing a useful strategy to refresh it periodically is more complicated (aha, now I understand why @djspiewak suggested we need Temporal here, to implement this refreshing strategy). In any case, all these complications still don't necessarily buy us any additional accuracy (although I would certainly believe this is fast!) when still many implementers could directly provide a high-accuracy F[FiniteDuration]. Now that I've thought this out a bit more (apologies Daniel :) I see its value, but seems somewhat orthogonal to the goal here (precise time vs fast time).

@armanbilge

Copy link
Copy Markdown
Member

One more additional followup question. Daniel mentioned that his offset strategy is

As a bonus, it's also about an order of magnitude faster to do this.

Like, why is that? At least according to this System.currentTimeMillis() and System.nanoTime() both involve a kernel call:
https://www.javaadvent.com/2019/12/measuring-time-from-java-to-kernel-and-back.html

So it's not obvious to me what we'd gain with fastRealTime.

@djspiewak

Copy link
Copy Markdown
Member

@brendanmaguire Okay so looping back to this! Finally! :-P

After a lot of thought and things, new plan. Let me know if you'd be interested in implementing the following:

  • New method on cats.effect.unsafe.Scheduler, something like nowMicros. This would need to have a default implementation based on nowMillis, then overridden in IORuntime's default scheduler to use Instant.now
  • Switch the implementation of RealTime in IOFiber to read from nowMicros rather than nowMillis
  • Add a new method to Temporal, something like fastRealTime (or some other name) which takes a FiniteDuration representing the maximum amount of time to wait before refreshing from realTime and which returns an F[F[FiniteDuration]] where the inner effect returns real time using the offset strategy.

That last one could just be a separate PR. Anyway, wdyt? Wanted to give you first crack at it since you've been patiently waiting for a review for more than six months.

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

Hey @djspiewak . Thanks for getting back to this 🙂 The approach sounds good 👍

Yes, I'd be happy to give this a go over the coming week, time permitting. If you would like it sooner then please feel free to hand it off to someone else.

@djspiewak

Copy link
Copy Markdown
Member

@brendanmaguire Go for it!

@armanbilge armanbilge added this to the v3.4.0 milestone Mar 17, 2022
…the JVM

* Use `java.time.Instant` to provide microsecond precision for `nowMicros`
* Use `nowMicros` instead of `nowMillis` to determine the real time
* Provide default implementation of `Scheduler#nowMicros`

This is a follow up to typelevel#1461
@brendanmaguire
brendanmaguire force-pushed the support-microsecond-precision-on-jvm branch from 38b2f53 to b58ae18 Compare March 20, 2022 16:31
@brendanmaguire

Copy link
Copy Markdown
Contributor Author

Just getting to look at this now. I think my original PR pretty much implements what you're suggesting in your first two bullet points @djspiewak 🤔 I've rebased on top of the latest 3.x changes and pushed. Am I missing anything to fulfil those two requirements?

djspiewak
djspiewak previously approved these changes Mar 20, 2022

@djspiewak djspiewak left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks correct to me! Thank you! So we can merge this and then a second PR can take on the FiniteDuration => F[F[FiniteDuration]] function.

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

This looks correct to me! Thank you! So we can merge this

Thanks @djspiewak 🙂

and then a second PR can take on the FiniteDuration => F[F[FiniteDuration]] function.

I'm afraid I don't fully understand what's required for this. I'm happy to try it if you could give further guidance on what's involved, or if someone else wants to take it on that's cool with me either.

@djspiewak

Copy link
Copy Markdown
Member

I'm afraid I don't fully understand what's required for this. I'm happy to try it if you could give further guidance on what's involved, or if someone else wants to take it on that's cool with me either.

So here's the idea… The advantage to calling realTime is that you get true wall-clock system time, taking into account any changes and adjustments that have been made since the last time you called (e.g. NTP sync cycles, manual clock changes, etc). The thing is that most application don't need that level of precision. Or at the very least, they don't need it with absolute per-millisecond verification. In most cases, if NTP sync happens and the application's notion of "wall clock" time fails to take the update into account for a few seconds (or even a few minutes!), this is okay. Some application simply don't care at all. You get the idea.

We can leverage this to make a much, much faster version of realTime which caches the syscall for some TTL and just offsets from that moment by using monotonic. Basically the concept is, call realTime and monotonic and calculate the offset between the two. Then, produce an effect which, when called, invokes monotonic and adjusts it by that offset. Keep doing this until the TTL has expired, at which point you need to refresh from realTime and re-calculate the offset.

The only thing tricky here is that there might be an autoyield between calls, which could mean the offset is wildly off. #2633 will fix this, but for the time being you can work around it by simply computing the offset twice consecutively and taking the min.

Anyway, the signature of this function should be something like Duration => F[F[FiniteDuration]]. Name subject to bike shedding, but something like cachedRealTime might be reasonable.

@djspiewak
djspiewak merged commit a045918 into typelevel:series/3.x Mar 20, 2022
@brendanmaguire
brendanmaguire deleted the support-microsecond-precision-on-jvm branch March 21, 2022 09:18
@brendanmaguire

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed explanation @djspiewak . Makes sense to me now. I will give it a go during the week.

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

Hey @djspiewak . I took a look at the cachedRealTime functionality and here's what I have so far:

  def cachedRealTime(refreshPeriod: Duration): F[F[FiniteDuration]] = {
    val offsetF = flatMap(realTime)(realTimeNow => map(monotonic)(realTimeNow - _))
    val minOffsetF = flatMap(offsetF)(offset => map(offsetF)(offset.min))
    map(minOffsetF)(cachedOffset => map(monotonic)(_ + cachedOffset))
  }

I'm not sure how to go about updating the cachedOffset after the refreshPeriod has elapsed. I could do it with a Ref but that would involve providing a Ref.Make[F] to GenTemporal which I'm sure would be undesirable.

Is there an approach here I'm missing?

Thanks!

@armanbilge

Copy link
Copy Markdown
Member

@brendanmaguire GenTemporal extends GenConcurrent which should have a ref method available :)

@brendanmaguire

Copy link
Copy Markdown
Contributor Author

@brendanmaguire GenTemporal extends GenConcurrent which should have a ref method available :)

Great! Thanks @armanbilge 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants