Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Clock interface, remove TimeProvider #2998

Merged
merged 24 commits into from
Oct 28, 2022

Conversation

devinrsmith
Copy link
Member

@devinrsmith devinrsmith commented Oct 13, 2022

Updates the Clock interface providing a much richer selection of methods, allowing the caller to best specify how they actually want to use the results. Removes TimeProvider interface and replaces with Clock. Allows the removal of a lot of extraneous allocation. An allocation-free Clock implementation is provided, but gracefully falls back to the Java UTC clock / Instant implementation.

Fixes #2992

…llers to more accurately describe what they are trying to do.

Fixes deephaven#2992
@devinrsmith devinrsmith added this to the Oct 2022 milestone Oct 13, 2022
@devinrsmith devinrsmith self-assigned this Oct 13, 2022
@devinrsmith
Copy link
Member Author

This is somewhat orthogonal to #2999, but there will be some conflicts in either case.

This PR is more about the clock interface and plumbing the callers intentions as deep as possible; while the other PR is more about making an allocation free system clock.

@devinrsmith devinrsmith marked this pull request as ready for review October 14, 2022 14:18
/**
* Real-time local clock impl with a delta adjustment for all time values.
*/
public class DeltaLocalClockImpl implements Clock, Serializable {
Copy link
Contributor

Choose a reason for hiding this comment

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

We use this in scripts sometimes when setting up a replayer.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll bring it back.

*
* @return the nano time
*/
long nanoTime();
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure this belongs here...seems like we should just use System.nanoTime() and exclude this from the interface. If you want possibly-fake nanos we have currentTimeNanos() — what are the situations where it really would be right to use this nanoTime() instead of System.nanoTime()?

Copy link
Member Author

@devinrsmith devinrsmith Oct 14, 2022

Choose a reason for hiding this comment

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

The idea is that you might want to replay the behavior that you would otherwise get w/ System.nanoTime() (either for unit test, or replaying data), but you don't want to pay the price of currentTimeNanos() in production.

For example, to use AutoTuningIncrementalReleaseFilter, there are three options:

  1. Clock.nanoTime() - step replayable; fast nanoTime in production
  2. System.nanoTime() - not replayable; fast nanoTime in production
  3. Clock.currentTimeNanos() - step replayable; slower currentTimeNanos in production

If you don't care about replayability / unit-testing wrt your timings, then you should use System.nanoTime().

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I'm still a bit skeptical of the legitimacy of case 1, but it does make the interface more complete


@Override
public final DateTime currentTime() {
return new DateTime(currentTimeNanos());
Copy link
Contributor

Choose a reason for hiding this comment

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

I think get rid of this class and just make this a default method in TimeProvider. You have to use nanos as the source for any TimeProvider anyway, since a TimeProvider's only job is providing a DateTime and you're limited to a single long when making a DateTime anyway

Copy link
Member Author

@devinrsmith devinrsmith Oct 14, 2022

Choose a reason for hiding this comment

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

I'm strongly anti-default methods - I think it muddies the water, and potentially provides places where calls are virtualized where they otherwise wouldn't need to be.

For example, io.deephaven.engine.table.impl.replay.DataDrivenReplayer is explicitly based off of DateTime; if we accidentally forget to override currentTime(), then we'd still get correct, but inefficient, behavior.

I know we use it in a lot of other places, but if I had my way I would remove most of them.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, it's completely acceptable to create a TimeProvider wholly from System.currentTimeMillis() if you wanted.

@@ -2336,4 +2376,38 @@ public static String getPartitionFromTimestampSeconds(@NotNull final DateTimeFor
}
return dateTimeFormatter.format(Instant.ofEpochMilli(timestampSeconds * 1_000));
}

private enum TimeProviderImpl implements TimeProvider {
Copy link
Contributor

Choose a reason for hiding this comment

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

why enum?

I think it'd be more straightforward to just add this INSTANCE as a field in TimeProvider

Copy link
Member Author

@devinrsmith devinrsmith Oct 14, 2022

Choose a reason for hiding this comment

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

IMO, when stateless singleton structures can be represented as enum, you gain niceties - equality, hashcode, etc for free.

Base/src/main/java/io/deephaven/base/clock/DeltaClock.java Outdated Show resolved Hide resolved
*
* @return the nano time
*/
long nanoTime();
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, I'm still a bit skeptical of the legitimacy of case 1, but it does make the interface more complete

rbasralian
rbasralian previously approved these changes Oct 25, 2022
'--add-opens=java.management/sun.management=ALL-UNNAMED',
# Allow our clock-impl project to access internals
'--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED',
Copy link
Contributor

Choose a reason for hiding this comment

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

could just use --add-exports instead of --add-opens — maybe vaguely better not to enable reflection where we don't need it?

Copy link
Member Author

Choose a reason for hiding this comment

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

--add-opens is necessary from the runtime perspective. --add-exports is a compile-time flag.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe there is some nuance I'm missing, will test.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great find - --add-opens is more aggressive than --add-exports in allowing reflective access; something we don't need for hotspot-impl nor clock-impl. Updated to reflect this.

rbasralian
rbasralian previously approved these changes Oct 25, 2022
*
* @see SystemClock
*/
public interface Clock {
Copy link
Member

Choose a reason for hiding this comment

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

Might want to ask @chipkent to look at your interface.


@Override
public final long nanoTime() {
return currentTimeNanos();
Copy link
Member

Choose a reason for hiding this comment

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

This is a kind of questionable implementation, since nanoTime() has different semantics than the other methods.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is a larger question of "Should we have unit-testability for nanoTime logic; and should that interface be on Clock?"

Confirmed w/ Charles, we might want fine replayability of nanoTime for unit tests, even though we don't have them right now.

The next part is "should this be part of clock?"

It may be reasonable to have a separate interface since it is more bespoke than Clock; but what I don't want to encourage is users just deciding to use Clock#currentTimeNanos because it's more convenient than plumbing through an additional nanoTime interface. That said, there aren't many users of Clock#nanoTime (only AutoTuningIncrementalReleaseFilter and BaseIncrementalReleaseFilter), so it would be a small change against this PR.

Options:

  1. Leave Clock#nanoTime as-is
  2. Create new interface MeasureNanos.
  3. Remove Clock#nanoTime, and explicitly use System.nanoTime in BaseIncrementalReleaseFilter/AutoTuningIncrementalReleaseFilter

I'm actually leaning 3 > 2 > 1; at least until we get to the point where we need nanoTime replayability for unit tests.

Copy link
Member

@rcaudy rcaudy Oct 27, 2022

Choose a reason for hiding this comment

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

I kind of like #3 for now. It feels better not to conflate sources of wall-clock time with sources of monotonically-increasing measurement time. I like your point about #1 making it explicit, though.

Copy link
Member Author

Choose a reason for hiding this comment

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

Proceeded w/ 3


@Override
public Instant instantNanos() {
return delegate.instantNanos().plusNanos(deltaNanos);
Copy link
Member

Choose a reason for hiding this comment

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

We'd be better off implementing this and instantMillis() in terms of nanos, right? This allocates 2 Instants.

Copy link
Member Author

Choose a reason for hiding this comment

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

So, I'm not as concerned w/ the efficiency of non-standard clock implementations - regardless, I think the current implementation is the "correct" implementation, given that we should always delegate the same method to delegate given it knows more about its implementation than DeltaClock does.

If we really needed to optimize for applying deltas to clocks, that would suggest we should instead have an interface:

interface Clock {
...
public Instant instantNanos(long deltaNanos);
...
}

But I think we can agree that would be overkill.

settings.gradle Show resolved Hide resolved
@@ -20,8 +22,33 @@ public DataDrivenReplayer(DateTime startTime, DateTime endTime) {
}

@Override
public DateTime currentTime() {
return currentTime;
public long currentTimeMillis() {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can move this up a level to avoid repetition, delegating to an abstract accessor for currentTime.

Copy link
Member Author

Choose a reason for hiding this comment

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

So, the hierarchy right now is a bit confusing, with DataDrivenReplayer and FixedStepReplayer (extends io.deephaven.engine.table.impl.replay.Replayer) having completely separate overrides than the parent class.

I think it could be cleaned up, but it would be a pretty major gutting that would be better done in isolation IMO.

/**
* Replay historical data as simulated real-time data.
*/
public class Replayer implements ReplayerInterface, Runnable {
private static final Logger log = LoggerFactory.getLogger(ShiftObliviousInstrumentedListener.class);
private static final int MILLIS_TO_NANOS_FACTOR = 1_000_000;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we want to prefer using TimeUnit?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll delegate back to DateTimeUtils.millisToNanos etc. And remove ENABLE_MICROTIME_HACK in the process.

@@ -43,7 +44,12 @@ protected void setUp() throws Exception {
super.setUp();

now = new MutableLong(0);
timeProvider = () -> new DateTime(now.longValue());
clock = new ClockNanoBase() {
Copy link
Member

Choose a reason for hiding this comment

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

We do this a lot. Maybe we want one based on a MutableLong?

Copy link
Member Author

Choose a reason for hiding this comment

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

Elevated TestClock.

@devinrsmith devinrsmith changed the title Presents more complete Clock and TimeProvider interfaces, allowing ca… Update Clock interface, remove TimeProvider Oct 27, 2022
rcaudy
rcaudy previously approved these changes Oct 27, 2022
Comment on lines 16 to 23
/**
* The {@link SystemClock}. Provides singleton semantics around {@link SystemClock#of()}.
*
* @return the system clock
*/
static SystemClock systemUTC() {
return SystemClockInstance.INSTANCE;
}
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this method belongs. It is specific to a single clock, and that clock can be obtained via other methods.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the method may actually be better named system()...

That said, it does serve an important service not fulfilled by other methods - it's essentially providing a singleton cache around SystemClock.of(). (SystemClockInstance itself is not public.)


import java.io.IOException;
import java.util.TimerTask;

/**
* An interface for replaying historical data as simulated real-time data.
*/
public interface ReplayerInterface {
public interface ReplayerInterface extends Clock {
Copy link
Member

Choose a reason for hiding this comment

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

Thoughts on making the replayer instance a Clock vs returning a Clock? I might lean to the second, since it seems funny to say that the replayer is a clock. Also, "is a Clock" is causing a bunch of methods to be duplicated in many files.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm happy to change this to "replayer returns a clock" but it's not going to solve any of the duplication - the replayer and classes that extend replayer seem to be built fundamentally different, so I don't think there's an easy path to de-duplication.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did find a slight way to simplify this by doing "returns a Clock" - and I agree it's a better interface.

Comment on lines -157 to +205
long epochSecond = nanos / 1000000000;
long nanoAdjustment = nanos % 1000000000;
return Instant.ofEpochSecond(epochSecond, nanoAdjustment);
return Instant.ofEpochSecond(0, nanos);
Copy link
Member

Choose a reason for hiding this comment

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

This looked correct before.

Copy link
Member Author

@devinrsmith devinrsmith Oct 28, 2022

Choose a reason for hiding this comment

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

This is a slight improvement, we don't have to do the math ourselves. The internals of ofEpochSecond can deal directly w/ long nanos.

*/
public static TimeProvider timeProvider;
public static Clock clock;
Copy link
Member

Choose a reason for hiding this comment

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

Having a static clock here is funky, since we can support multiple replayers, where we probably want the clock to be set based upon the context of the replayer. We are certainly no worse than we were, but we should have a ticket to address this at some point.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree - it would be much better to inject and pass along a Clock instead of having a static. I'll create an issue and link it from DateTimeUtils.

@devinrsmith devinrsmith merged commit 76b42f9 into deephaven:main Oct 28, 2022
@devinrsmith devinrsmith deleted the better-clock-timeprovider branch October 28, 2022 22:16
@github-actions github-actions bot locked and limited conversation to collaborators Oct 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MicroTimer/DateTimeUtils.currentTime()/DateTime.now() can return wrong time
4 participants