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

Use java.time.Instant to get micros accurate timestamps #261

Merged
merged 4 commits into from
Oct 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -21,8 +21,6 @@

import co.elastic.apm.objectpool.Recyclable;

import java.util.concurrent.TimeUnit;

/**
* This clock makes sure that each {@link Span} and {@link Transaction} uses a consistent clock
* which does not drift in case of NTP updates or leap seconds.
Expand Down Expand Up @@ -52,19 +50,19 @@ public void init(EpochTickClock other) {
* @return the epoch microsecond timestamp at initialization time
*/
public long init() {
return init(System.currentTimeMillis(), System.nanoTime());
return init(SystemClock.ForCurrentVM.INSTANCE.getEpochMicros(), System.nanoTime());
}

/**
* Initializes and calibrates the clock based on wall clock time
*
* @param epochMillis the current timestamp in milliseconds since epoch (mostly {@link System#currentTimeMillis()})
* @param nanoTime the current nanosecond precision timestamp (mostly {@link System#nanoTime()}
* @param epochMicrosWallClock the current timestamp in microseconds since epoch, based on wall clock time
* @param nanoTime the current nanosecond ticks (mostly {@link System#nanoTime()}
* @return the epoch microsecond timestamp at initialization time
*/
public long init(long epochMillis, long nanoTime) {
nanoTimeOffsetToEpoch = TimeUnit.MILLISECONDS.toNanos(epochMillis) - nanoTime;
return TimeUnit.MILLISECONDS.toMicros(epochMillis);
public long init(long epochMicrosWallClock, long nanoTime) {
nanoTimeOffsetToEpoch = epochMicrosWallClock * 1_000 - nanoTime;
return epochMicrosWallClock;
}

public long getEpochMicros() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*-
* #%L
* Elastic APM Java agent
* %%
* Copyright (C) 2018 Elastic and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package co.elastic.apm.impl.transaction;

import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;

import java.time.Clock;
import java.time.Instant;

public interface SystemClock {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's mark with a TODO or something that will let us quickly find all places we can cut unnecessary code when dropping support to 1.7.

Copy link
Member Author

Choose a reason for hiding this comment

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

We just have to look out for the usages of the @IgnoreJRERequirement annotation. The animal sniffer maven plugin would not let us verify non-Java 7 code which is not annotated with this annotation.


long getEpochMicros();

enum ForCurrentVM implements SystemClock {
INSTANCE;
private final SystemClock dispatcher;

ForCurrentVM() {
SystemClock localDispatcher;
try {
// being cautious to not cause linking of ForJava8CapableVM in case we are not running on Java 8+
Class.forName("java.time.Clock");
localDispatcher = (SystemClock) Class.forName(SystemClock.class.getName() + "$ForJava8CapableVM").getEnumConstants()[0];
} catch (Exception e) {
localDispatcher = ForLegacyVM.INSTANCE;
}
dispatcher = localDispatcher;
}

@Override
public long getEpochMicros() {
return dispatcher.getEpochMicros();
}
}

@IgnoreJRERequirement
enum ForJava8CapableVM implements SystemClock {
INSTANCE;

private static final Clock clock = Clock.systemUTC();
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be risky - this class cannot be fully loaded (pass linkage phase) on JRE 7. This means that ForCurrentVM cannot be fully linked as well as it depends on it. You may not experience any problem using a lazy-linkage JVM, one that allows only linking parts of the class's constant pool when required, but I am not sure it is safe to assume that will be the case in all JVMs as I don't think the lazy-linkage is part of the JVM spec.
Using reflection referencing a Method or MethodHandle would be a safer approach

Copy link
Member Author

Choose a reason for hiding this comment

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

You are completely right - that would be safer. But we don't support such a VM so I would not want to pay the cost of the increased complexity and potentially higher overhead yet. We can't claim to support a particular VM if we don't have an integration test for it. If we are to support such a VM in the future at that time, we might even have already dropped Java 7 support so that we can just use Instant right away.


@Override
public long getEpochMicros() {
// escape analysis, plz kick in and allocate the Instant on the stack
final Instant now = clock.instant();
return now.getEpochSecond() * 1_000_000 + now.getNano() / 1_000;
}
}

enum ForLegacyVM implements SystemClock {
INSTANCE;

@Override
public long getEpochMicros() {
return System.currentTimeMillis() * 1_000;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand Down Expand Up @@ -37,9 +37,9 @@ void setUp() {

@Test
void testEpochMicros() {
final long epochMillis = System.currentTimeMillis();
epochTickClock.init(epochMillis, 0);
final long epochMicros = SystemClock.ForJava8CapableVM.INSTANCE.getEpochMicros();
epochTickClock.init(epochMicros, 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Unit-test JRE 7 code as well (regardless of the actual JRE running the tests)

Copy link
Member Author

Choose a reason for hiding this comment

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

done

final int nanoTime = 1000;
assertThat(epochTickClock.getEpochMicros(nanoTime)).isEqualTo(epochMillis * 1000 + TimeUnit.NANOSECONDS.toMicros(nanoTime));
assertThat(epochTickClock.getEpochMicros(nanoTime)).isEqualTo(epochMicros + TimeUnit.NANOSECONDS.toMicros(nanoTime));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package co.elastic.apm.impl.transaction;

import org.junit.jupiter.api.Test;

import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.Offset.offset;

class SystemClockTest {

@Test
void testClocks() {
final long currentVmEpochMicros = SystemClock.ForCurrentVM.INSTANCE.getEpochMicros();
final long java8EpochMicros = SystemClock.ForJava8CapableVM.INSTANCE.getEpochMicros();
final long java7EpochMicros = SystemClock.ForLegacyVM.INSTANCE.getEpochMicros();
assertThat(java8EpochMicros).isCloseTo(java7EpochMicros, offset(TimeUnit.SECONDS.toMicros(10)));
assertThat(java8EpochMicros).isCloseTo(currentVmEpochMicros, offset(TimeUnit.SECONDS.toMicros(10)));
assertThat(java7EpochMicros % 1000).isEqualTo(0);
}
}