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

[MNG-8533] [MNG-5729] Use monotonic time measurements #1965

Merged
merged 12 commits into from
Dec 12, 2024
6 changes: 6 additions & 0 deletions api/maven-api-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@
<groupId>org.apache.maven</groupId>
<artifactId>maven-api-di</artifactId>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.maven.api;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalQueries;
import java.time.temporal.TemporalQuery;
import java.time.temporal.TemporalUnit;
import java.time.temporal.UnsupportedTemporalTypeException;
import java.time.temporal.ValueRange;
import java.util.concurrent.TimeUnit;

/**
* A time measurement class that combines monotonic timing with wall-clock time.
* <p>
* This class provides precise duration measurements using {@link System#nanoTime()}
* while also maintaining wall-clock time information in UTC. The wall-clock time
* is computed from the monotonic duration since system start to ensure consistency
* between time measurements.
* <p>
* All wall-clock times are handled in UTC to maintain consistency and avoid
* timezone/DST complexities. Users needing local time representation should
* convert the result of {@link #getWallTime()} to their desired timezone:
* <pre>{@code
* MonotonicTime time = MonotonicTime.now();
* // Get local time with DST handling:
* ZonedDateTime local = time.getWallTime().atZone(ZoneId.systemDefault());
* }</pre>
*/
public final class MonotonicTime implements Temporal {

/**
* Reference point representing the time when this class was first loaded.
* Uses UTC for wall-clock time representation.
*/
public static final MonotonicTime START =
new MonotonicTime(System.nanoTime(), Clock.systemUTC().instant());

private final long nanoTime;
private volatile Instant wallTime;

// Opened for testing
MonotonicTime(long nanoTime, Instant wallTime) {
this.nanoTime = nanoTime;
this.wallTime = wallTime;
}

/**
* Creates a new {@code MonotonicTime} instance capturing the current time.
* Wall-clock time will be computed in UTC when needed.
*
* @return a new {@code MonotonicTime} instance
*/
public static MonotonicTime now() {
return new MonotonicTime(System.nanoTime(), null);
}

/**
* Returns the raw monotonic time value from System.nanoTime().
* <p>
* This value represents a monotonic time measurement that can only be compared
* with other MonotonicTime instances obtained within the same JVM session.
* The absolute value has no meaning on its own and is not related to any epoch
* or wall clock time.
* <p>
* This value has nanosecond precision but not necessarily nanosecond accuracy -
* the actual precision depends on the underlying system.
* <p>
* For timing intervals, prefer using {@link #durationSince(MonotonicTime)} instead
* of manually calculating differences between nanoTime values.
*
* @return the raw nanosecond value from System.nanoTime()
* @see System#nanoTime()
*/
public long getNanoTime() {
return nanoTime;
}

/**
* Calculates the duration between this time and {@link #START}.
* This measurement uses monotonic time and is not affected by system clock changes.
*
* @return the duration since JVM startup
*/
public Duration durationSinceStart() {
return durationSince(START);
}

/**
* Calculates the duration between this time and the specified start time.
* This measurement uses monotonic time and is not affected by system clock changes.
*
* @param start the starting point for the duration calculation
* @return the duration between the start time and this time
*/
public Duration durationSince(MonotonicTime start) {
return Duration.ofNanos(this.nanoTime - start.nanoTime);
}

/**
* Returns the wall clock time for this instant, computed from START's wall time
* and the monotonic duration since START. The time is always in UTC.
* <p>
* For local time representation, convert the result using {@link Instant#atZone(ZoneId)}:
* <pre>{@code
* ZonedDateTime localTime = time.getWallTime().atZone(ZoneId.systemDefault());
* }</pre>
*
* @return the {@link Instant} representing the UTC wall clock time
*/
public Instant getWallTime() {
Instant local = wallTime;
if (local == null) {
synchronized (this) {
local = wallTime;
if (local == null) {
local = START.getWallTime().plus(durationSince(START));
wallTime = local;
}
}
}
return local;
}

/**
* Creates a {@code MonotonicTime} from a millisecond timestamp.
* <p>
* <strong>WARNING:</strong> This method is inherently unsafe and should only be used
* for legacy integration. It attempts to create a monotonic time measurement from
* a wall clock timestamp, which means:
* <ul>
* <li>The monotonic timing will be imprecise (millisecond vs. nanosecond precision)</li>
* <li>Duration calculations may be incorrect due to system clock adjustments</li>
* <li>The relationship between wall time and monotonic time will be artificial</li>
* <li>Comparisons with other MonotonicTime instances will be meaningless</li>
* </ul>
*
* @param epochMillis milliseconds since Unix epoch (from System.currentTimeMillis())
* @return a new {@code MonotonicTime} instance
* @deprecated This method exists only for legacy integration. Use {@link #now()}
* for new code.
*/
@Deprecated(since = "4.0.0", forRemoval = true)
public static MonotonicTime ofEpochMillis(long epochMillis) {
Instant wallTime = Instant.ofEpochMilli(epochMillis);
// Converting to nanos but this relationship is artificial
long artificalNanoTime = TimeUnit.MILLISECONDS.toNanos(epochMillis);
return new MonotonicTime(artificalNanoTime, wallTime);
}

@Override
public boolean isSupported(TemporalField field) {
if (field == ChronoField.OFFSET_SECONDS) {
return true;
}
return getWallTime().isSupported(field);
}

@Override
public long getLong(TemporalField field) {
if (field == ChronoField.OFFSET_SECONDS) {
return 0; // UTC has zero offset
}
// Handle calendar fields by converting to ZonedDateTime
if (field instanceof ChronoField && !getWallTime().isSupported(field)) {
return getWallTime().atZone(ZoneId.of("UTC")).getLong(field);
}
return getWallTime().getLong(field);
}

@Override
public boolean isSupported(TemporalUnit unit) {
if (!(unit instanceof ChronoUnit chronoUnit)) {
return false;
}
return chronoUnit.isTimeBased() && !chronoUnit.isDurationEstimated();
}

@Override
public Temporal with(TemporalField field, long newValue) {
throw new UnsupportedTemporalTypeException("MonotonicTime does not support field adjustments");
}

@Override
public long until(Temporal endExclusive, TemporalUnit unit) {
if (!(unit instanceof ChronoUnit) || !isSupported(unit)) {
throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
}

if (endExclusive instanceof MonotonicTime other) {
Duration duration = Duration.ofNanos(other.nanoTime - this.nanoTime);
return switch ((ChronoUnit) unit) {
case NANOS -> duration.toNanos();
case MICROS -> duration.toNanos() / 1000;
case MILLIS -> duration.toMillis();
case SECONDS -> duration.toSeconds();
case MINUTES -> duration.toMinutes();
case HOURS -> duration.toHours();
default -> throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
};
}

return unit.between(getWallTime(), Instant.from(endExclusive));
}

@Override
public Temporal plus(long amountToAdd, TemporalUnit unit) {
throw new UnsupportedTemporalTypeException("MonotonicTime does not support plus operations");
}

@Override
@SuppressWarnings("unchecked")
public <R> R query(TemporalQuery<R> query) {
if (query == TemporalQueries.zoneId()) {
return (R) ZoneId.of("UTC");
}
if (query == TemporalQueries.precision()) {
return (R) ChronoUnit.NANOS;
}
if (query == TemporalQueries.zone()) {
return (R) ZoneId.of("UTC");
}
if (query == TemporalQueries.chronology()) {
return null;
}
return getWallTime().query(query);
}

@Override
public ValueRange range(TemporalField field) {
if (field == ChronoField.OFFSET_SECONDS) {
return field.range();
}
return getWallTime().range(field);
}

@Override
public boolean equals(Object obj) {
if (!(obj instanceof MonotonicTime other)) {
return false;
}
return other.nanoTime == this.nanoTime;
}

@Override
public int hashCode() {
return Long.hashCode(nanoTime);
}

@Override
public String toString() {
return String.format("MonotonicTime[wall=%s, duration=%s]", getWallTime(), Duration.ofNanos(nanoTime));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package org.apache.maven.api;

import java.nio.file.Path;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -64,7 +63,7 @@ public interface ProtoSession {
* @return the start time as an Instant object, never {@code null}
*/
@Nonnull
Instant getStartTime();
MonotonicTime getStartTime();

/**
* Gets the directory of the topmost project being built, usually the current directory or the
Expand Down Expand Up @@ -106,13 +105,13 @@ default Builder toBuilder() {
* Returns new builder from scratch.
*/
static Builder newBuilder() {
return new Builder().withStartTime(Instant.now());
return new Builder().withStartTime(MonotonicTime.now());
}

class Builder {
private Map<String, String> userProperties;
private Map<String, String> systemProperties;
private Instant startTime;
private MonotonicTime startTime;
private Path topDirectory;
private Path rootDirectory;

Expand All @@ -121,7 +120,7 @@ private Builder() {}
private Builder(
Map<String, String> userProperties,
Map<String, String> systemProperties,
Instant startTime,
MonotonicTime startTime,
Path topDirectory,
Path rootDirectory) {
this.userProperties = userProperties;
Expand All @@ -141,7 +140,7 @@ public Builder withSystemProperties(@Nonnull Map<String, String> systemPropertie
return this;
}

public Builder withStartTime(@Nonnull Instant startTime) {
public Builder withStartTime(@Nonnull MonotonicTime startTime) {
this.startTime = requireNonNull(startTime, "startTime");
return this;
}
Expand All @@ -163,14 +162,14 @@ public ProtoSession build() {
private static class Impl implements ProtoSession {
private final Map<String, String> userProperties;
private final Map<String, String> systemProperties;
private final Instant startTime;
private final MonotonicTime startTime;
private final Path topDirectory;
private final Path rootDirectory;

private Impl(
Map<String, String> userProperties,
Map<String, String> systemProperties,
Instant startTime,
MonotonicTime startTime,
Path topDirectory,
Path rootDirectory) {
this.userProperties = requireNonNull(userProperties);
Expand All @@ -191,7 +190,7 @@ public Map<String, String> getSystemProperties() {
}

@Override
public Instant getStartTime() {
public MonotonicTime getStartTime() {
return startTime;
}

Expand Down
Loading
Loading