Skip to content

Commit 3f18606

Browse files
committed
[MNG-5729] Use monotonic time measurements
1 parent 6cb4482 commit 3f18606

39 files changed

+807
-172
lines changed

api/maven-api-core/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
<groupId>org.apache.maven</groupId>
6060
<artifactId>maven-api-di</artifactId>
6161
</dependency>
62+
63+
<dependency>
64+
<groupId>org.junit.jupiter</groupId>
65+
<artifactId>junit-jupiter-api</artifactId>
66+
<scope>test</scope>
67+
</dependency>
6268
</dependencies>
6369

6470
</project>
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.api;
20+
21+
import java.time.Duration;
22+
import java.time.Instant;
23+
import java.time.ZoneId;
24+
import java.time.temporal.ChronoField;
25+
import java.time.temporal.ChronoUnit;
26+
import java.time.temporal.Temporal;
27+
import java.time.temporal.TemporalField;
28+
import java.time.temporal.TemporalQueries;
29+
import java.time.temporal.TemporalQuery;
30+
import java.time.temporal.TemporalUnit;
31+
import java.time.temporal.UnsupportedTemporalTypeException;
32+
import java.time.temporal.ValueRange;
33+
import java.util.Objects;
34+
import java.util.concurrent.TimeUnit;
35+
36+
/**
37+
* Represents a point in time using both monotonic and wall clock measurements.
38+
* This class combines the monotonic precision of {@link System#nanoTime()} with
39+
* wall clock time from {@link Instant#now()}, providing accurate duration measurements
40+
* while maintaining human-readable timestamps.
41+
* <p>
42+
* This class implements {@link Temporal} by delegating to its computed wall time,
43+
* allowing it to be used with standard Java time formatting and querying operations.
44+
*/
45+
public final class MonotonicTime implements Temporal {
46+
47+
/**
48+
* Reference point representing the time when this class was first loaded.
49+
* Can be used as a global start time for duration measurements.
50+
*/
51+
public static final MonotonicTime START = new MonotonicTime(System.nanoTime(), Instant.now());
52+
53+
private final long nanoTime;
54+
private volatile Instant wallTime;
55+
56+
// Opened for testing
57+
MonotonicTime(long nanoTime, Instant wallTime) {
58+
this.nanoTime = nanoTime;
59+
this.wallTime = wallTime;
60+
}
61+
62+
/**
63+
* Creates a new {@code MonotonicTime} instance capturing the current time
64+
* using both monotonic and wall clock measurements.
65+
*
66+
* @return a new {@code MonotonicTime} instance
67+
*/
68+
public static MonotonicTime now() {
69+
return new MonotonicTime(System.nanoTime(), null);
70+
}
71+
72+
/**
73+
* Calculates the duration between this time and {@link #START}.
74+
* This measurement uses monotonic time and is not affected by system clock changes.
75+
*
76+
* @return the duration since JVM startup
77+
*/
78+
public Duration durationSinceStart() {
79+
return durationSince(START);
80+
}
81+
82+
/**
83+
* Calculates the duration between this time and the specified start time.
84+
* This measurement uses monotonic time and is not affected by system clock changes.
85+
*
86+
* @param start the starting point for the duration calculation
87+
* @return the duration between the start time and this time
88+
*/
89+
public Duration durationSince(MonotonicTime start) {
90+
return Duration.ofNanos(this.nanoTime - start.nanoTime);
91+
}
92+
93+
/**
94+
* Returns the raw monotonic time value from System.nanoTime().
95+
* <p>
96+
* This value represents a monotonic time measurement that can only be compared
97+
* with other MonotonicTime instances obtained within the same JVM session.
98+
* The absolute value has no meaning on its own and is not related to any epoch or wall clock time.
99+
* <p>
100+
* This value has nanosecond precision but not necessarily nanosecond accuracy -
101+
* the actual precision depends on the underlying system.
102+
* <p>
103+
* For timing intervals, prefer using {@link #durationSince(MonotonicTime)} instead of
104+
* manually calculating differences between nanoTime values.
105+
*
106+
* @return the raw nanosecond value from System.nanoTime()
107+
* @see System#nanoTime()
108+
*/
109+
public long getNanoTime() {
110+
return nanoTime;
111+
}
112+
113+
/**
114+
* Returns the wall clock time for this instant.
115+
* The time is computed lazily based on START's wall time and the monotonic duration since START.
116+
*
117+
* @return the {@link Instant} representing the wall clock time
118+
*/
119+
public Instant getWallTime() {
120+
Instant local = wallTime;
121+
if (local == null) {
122+
// Double-checked locking pattern
123+
synchronized (this) {
124+
local = wallTime;
125+
if (local == null) {
126+
local = START.getWallTime().plus(durationSince(START));
127+
wallTime = local;
128+
}
129+
}
130+
}
131+
return local;
132+
}
133+
134+
/**
135+
* Creates a {@code MonotonicTime} from a millisecond timestamp.
136+
* <p>
137+
* <strong>WARNING:</strong> This method is inherently unsafe and should only be used
138+
* for legacy integration. It attempts to create a monotonic time measurement from
139+
* a wall clock timestamp, which means:
140+
* <ul>
141+
* <li>The monotonic timing will be imprecise (millisecond vs. nanosecond precision)</li>
142+
* <li>Duration calculations may be incorrect due to system clock adjustments</li>
143+
* <li>The relationship between wall time and monotonic time will be artificial</li>
144+
* <li>Comparisons with other MonotonicTime instances will be meaningless</li>
145+
* </ul>
146+
*
147+
* @param epochMillis milliseconds since Unix epoch (from System.currentTimeMillis())
148+
* @return a new {@code MonotonicTime} instance
149+
* @deprecated This method exists only for legacy integration. Use {@link #now()}
150+
* for new code.
151+
*/
152+
@Deprecated(since = "4.0.0", forRemoval = true)
153+
public static MonotonicTime ofEpochMillis(long epochMillis) {
154+
Instant wallTime = Instant.ofEpochMilli(epochMillis);
155+
// Converting to nanos but this relationship is artificial
156+
long artificalNanoTime = TimeUnit.MILLISECONDS.toNanos(epochMillis);
157+
return new MonotonicTime(artificalNanoTime, wallTime);
158+
}
159+
160+
@Override
161+
public boolean isSupported(TemporalField field) {
162+
if (field == ChronoField.OFFSET_SECONDS) {
163+
return true;
164+
}
165+
return getWallTime().isSupported(field);
166+
}
167+
168+
@Override
169+
public boolean isSupported(TemporalUnit unit) {
170+
return unit instanceof ChronoUnit chronoUnit && chronoUnit.isTimeBased(); // This includes DAYS and below
171+
}
172+
173+
@Override
174+
public long getLong(TemporalField field) {
175+
if (field == ChronoField.OFFSET_SECONDS) {
176+
return 0; // We use UTC/Zero offset
177+
}
178+
return getWallTime().getLong(field);
179+
}
180+
181+
@Override
182+
public Temporal with(TemporalField field, long newValue) {
183+
throw new UnsupportedTemporalTypeException("MonotonicTime does not support field adjustments");
184+
}
185+
186+
@Override
187+
@SuppressWarnings("unchecked")
188+
public <R> R query(TemporalQuery<R> query) {
189+
if (query == TemporalQueries.zoneId()) {
190+
return (R) ZoneId.systemDefault();
191+
}
192+
if (query == TemporalQueries.precision()) {
193+
return (R) ChronoUnit.NANOS;
194+
}
195+
if (query == TemporalQueries.zone()) {
196+
return (R) ZoneId.systemDefault(); // Be consistent with zoneId query
197+
}
198+
if (query == TemporalQueries.chronology()) {
199+
return null;
200+
}
201+
return getWallTime().query(query);
202+
}
203+
204+
@Override
205+
public ValueRange range(TemporalField field) {
206+
return getWallTime().range(field);
207+
}
208+
209+
@Override
210+
public Temporal plus(long amountToAdd, TemporalUnit unit) {
211+
throw new UnsupportedTemporalTypeException("MonotonicTime does not support plus operations");
212+
}
213+
214+
@Override
215+
public long until(Temporal endExclusive, TemporalUnit unit) {
216+
if (!(unit instanceof ChronoUnit) || !isSupported(unit)) {
217+
throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
218+
}
219+
220+
if (endExclusive instanceof MonotonicTime other) {
221+
Duration duration = Duration.ofNanos(other.nanoTime - this.nanoTime);
222+
return switch ((ChronoUnit) unit) {
223+
case NANOS -> duration.toNanos();
224+
case MICROS -> duration.toNanos() / 1000;
225+
case MILLIS -> duration.toMillis();
226+
case SECONDS -> duration.toSeconds();
227+
case MINUTES -> duration.toMinutes();
228+
case HOURS -> duration.toHours();
229+
default -> throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
230+
};
231+
}
232+
233+
return unit.between(getWallTime(), Instant.from(endExclusive));
234+
}
235+
236+
@Override
237+
public int get(TemporalField field) {
238+
return getWallTime().get(field);
239+
}
240+
241+
@Override
242+
public boolean equals(Object obj) {
243+
if (!(obj instanceof MonotonicTime)) {
244+
return false;
245+
}
246+
MonotonicTime other = (MonotonicTime) obj;
247+
return other.nanoTime == this.nanoTime;
248+
}
249+
250+
@Override
251+
public int hashCode() {
252+
return Objects.hash(nanoTime);
253+
}
254+
255+
/**
256+
* Returns a string representation of this time, including both the wall clock time
257+
* and the duration since the Unix epoch.
258+
*
259+
* @return a string representation of this time
260+
*/
261+
@Override
262+
public String toString() {
263+
return String.format("MonotonicTime[wall=%s, duration=%s]", getWallTime(), Duration.ofNanos(nanoTime));
264+
}
265+
}

api/maven-api-core/src/main/java/org/apache/maven/api/ProtoSession.java

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
package org.apache.maven.api;
2020

2121
import java.nio.file.Path;
22-
import java.time.Instant;
2322
import java.util.HashMap;
2423
import java.util.Map;
2524

@@ -64,7 +63,7 @@ public interface ProtoSession {
6463
* @return the start time as an Instant object, never {@code null}
6564
*/
6665
@Nonnull
67-
Instant getStartTime();
66+
MonotonicTime getStartTime();
6867

6968
/**
7069
* Gets the directory of the topmost project being built, usually the current directory or the
@@ -106,13 +105,13 @@ default Builder toBuilder() {
106105
* Returns new builder from scratch.
107106
*/
108107
static Builder newBuilder() {
109-
return new Builder().withStartTime(Instant.now());
108+
return new Builder().withStartTime(MonotonicTime.now());
110109
}
111110

112111
class Builder {
113112
private Map<String, String> userProperties;
114113
private Map<String, String> systemProperties;
115-
private Instant startTime;
114+
private MonotonicTime startTime;
116115
private Path topDirectory;
117116
private Path rootDirectory;
118117

@@ -121,7 +120,7 @@ private Builder() {}
121120
private Builder(
122121
Map<String, String> userProperties,
123122
Map<String, String> systemProperties,
124-
Instant startTime,
123+
MonotonicTime startTime,
125124
Path topDirectory,
126125
Path rootDirectory) {
127126
this.userProperties = userProperties;
@@ -141,7 +140,7 @@ public Builder withSystemProperties(@Nonnull Map<String, String> systemPropertie
141140
return this;
142141
}
143142

144-
public Builder withStartTime(@Nonnull Instant startTime) {
143+
public Builder withStartTime(@Nonnull MonotonicTime startTime) {
145144
this.startTime = requireNonNull(startTime, "startTime");
146145
return this;
147146
}
@@ -163,14 +162,14 @@ public ProtoSession build() {
163162
private static class Impl implements ProtoSession {
164163
private final Map<String, String> userProperties;
165164
private final Map<String, String> systemProperties;
166-
private final Instant startTime;
165+
private final MonotonicTime startTime;
167166
private final Path topDirectory;
168167
private final Path rootDirectory;
169168

170169
private Impl(
171170
Map<String, String> userProperties,
172171
Map<String, String> systemProperties,
173-
Instant startTime,
172+
MonotonicTime startTime,
174173
Path topDirectory,
175174
Path rootDirectory) {
176175
this.userProperties = requireNonNull(userProperties);
@@ -191,7 +190,7 @@ public Map<String, String> getSystemProperties() {
191190
}
192191

193192
@Override
194-
public Instant getStartTime() {
193+
public MonotonicTime getStartTime() {
195194
return startTime;
196195
}
197196

0 commit comments

Comments
 (0)