Skip to content

Commit

Permalink
[MNG-5729] Use monotonic time measurements (#1965)
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet authored Dec 12, 2024
1 parent 5b7a6de commit 54ffc50
Show file tree
Hide file tree
Showing 43 changed files with 503 additions and 249 deletions.
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
Expand Up @@ -437,5 +437,13 @@ public final class Constants {
@Config(type = "java.lang.Integer")
public static final String MAVEN_DEPLOY_SNAPSHOT_BUILD_NUMBER = "maven.deploy.snapshot.buildNumber";

/**
* User property used to store the build timestamp.
*
* @since 4.1.0
*/
@Config(type = "java.time.Instant")
public static final String MAVEN_START_INSTANT = "maven.startInstant";

private Constants() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;

/**
* A Clock implementation that combines monotonic timing with wall-clock time.
* <p>
* This class provides precise time measurements using {@link System#nanoTime()}
* while 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>
* This implementation is singleton-based and always uses UTC timezone. The clock
* cannot be adjusted to different timezones to maintain consistent monotonic behavior.
* Users needing local time representation should convert the result of {@link #instant()}
* to their desired timezone:
* <pre>{@code
* Instant now = MonotonicClock.now();
* ZonedDateTime local = now.atZone(ZoneId.systemDefault());
* }</pre>
*
* @see System#nanoTime()
* @see Clock
*/
public class MonotonicClock extends Clock {
private static final MonotonicClock CLOCK = new MonotonicClock();

private final long startNanos;
private final Instant startInstant;

/**
* Private constructor to enforce singleton pattern.
* Initializes the clock with the current system time and nanoTime.
*/
private MonotonicClock() {
this.startNanos = System.nanoTime();
this.startInstant = Clock.systemUTC().instant();
}

/**
* Returns the singleton instance of MonotonicClock.
*
* @return the monotonic clock instance
*/
public static MonotonicClock get() {
return CLOCK;
}

/**
* Returns the current instant from the monotonic clock.
* This is a convenience method equivalent to {@code get().instant()}.
*
* @return the current instant using monotonic timing
*/
public static Instant now() {
return get().instant();
}

/**
* Returns a monotonically increasing instant.
* <p>
* The returned instant is calculated by adding the elapsed nanoseconds
* since clock creation to the initial wall clock time. This ensures that
* the time never goes backwards and maintains a consistent relationship
* with the wall clock time.
*
* @return the current instant using monotonic timing
*/
@Override
public Instant instant() {
long elapsedNanos = System.nanoTime() - startNanos;
return startInstant.plusNanos(elapsedNanos);
}

/**
* Returns the zone ID of this clock, which is always UTC.
*
* @return the UTC zone ID
*/
@Override
public ZoneId getZone() {
return ZoneOffset.UTC;
}

/**
* Returns this clock since timezone adjustments are not supported.
* <p>
* This implementation maintains UTC time to ensure monotonic behavior.
* The provided zone parameter is ignored.
*
* @param zone the target timezone (ignored)
* @return this clock instance
*/
@Override
public Clock withZone(ZoneId zone) {
// Monotonic clock is always UTC-based
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ default Builder toBuilder() {
* Returns new builder from scratch.
*/
static Builder newBuilder() {
return new Builder().withStartTime(Instant.now());
return new Builder().withStartTime(MonotonicClock.now());
}

class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@

import java.io.File;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.List;
import java.util.Objects;

import org.apache.maven.api.MonotonicClock;
import org.apache.maven.api.services.MessageBuilder;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.apache.maven.execution.AbstractExecutionListener;
Expand Down Expand Up @@ -223,7 +227,7 @@ private void logReactorSummary(MavenSession session) {
} else if (buildSummary instanceof BuildSuccess) {
buffer.append(builder().success("SUCCESS"));
buffer.append(" [");
String buildTimeDuration = formatDuration(buildSummary.getTime());
String buildTimeDuration = formatDuration(buildSummary.getExecTime());
int padSize = MAX_PADDED_BUILD_TIME_DURATION_LENGTH - buildTimeDuration.length();
if (padSize > 0) {
buffer.append(chars(' ', padSize));
Expand All @@ -233,7 +237,7 @@ private void logReactorSummary(MavenSession session) {
} else if (buildSummary instanceof BuildFailure) {
buffer.append(builder().failure("FAILURE"));
buffer.append(" [");
String buildTimeDuration = formatDuration(buildSummary.getTime());
String buildTimeDuration = formatDuration(buildSummary.getExecTime());
int padSize = MAX_PADDED_BUILD_TIME_DURATION_LENGTH - buildTimeDuration.length();
if (padSize > 0) {
buffer.append(chars(' ', padSize));
Expand Down Expand Up @@ -266,15 +270,15 @@ private MessageBuilder builder() {
private void logStats(MavenSession session) {
infoLine('-');

long finish = System.currentTimeMillis();
Instant finish = MonotonicClock.now();

long time = finish - session.getRequest().getStartTime().getTime();
Duration time = Duration.between(session.getRequest().getStartInstant(), finish);

String wallClock = session.getRequest().getDegreeOfConcurrency() > 1 ? " (Wall Clock)" : "";

logger.info("Total time: {}{}", formatDuration(time), wallClock);

logger.info("Finished at: {}", formatTimestamp(finish));
logger.info("Finished at: {}", formatTimestamp(finish.atZone(ZoneId.systemDefault())));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -33,6 +32,7 @@
import org.apache.maven.InternalErrorException;
import org.apache.maven.Maven;
import org.apache.maven.api.Constants;
import org.apache.maven.api.MonotonicClock;
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.cli.Logger;
import org.apache.maven.api.cli.mvn.MavenOptions;
Expand Down Expand Up @@ -105,7 +105,7 @@ protected MavenExecutionRequest prepareMavenExecutionRequest() throws Exception
mavenExecutionRequest.setIgnoreMissingArtifactDescriptor(true);
mavenExecutionRequest.setRecursive(true);
mavenExecutionRequest.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_FAST);
mavenExecutionRequest.setStartTime(new Date());
mavenExecutionRequest.setStartInstant(MonotonicClock.now());
mavenExecutionRequest.setLoggingLevel(MavenExecutionRequest.LOGGING_LEVEL_INFO);
mavenExecutionRequest.setDegreeOfConcurrency(1);
mavenExecutionRequest.setBuilderId("singlethreaded");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
package org.apache.maven.cling.transfer;

import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;

import org.apache.maven.api.MonotonicClock;
import org.apache.maven.api.services.MessageBuilder;
import org.apache.maven.api.services.MessageBuilderFactory;
import org.eclipse.aether.transfer.AbstractTransferListener;
Expand Down Expand Up @@ -80,11 +83,12 @@ public void transferSucceeded(TransferEvent event) {
message.resetStyle().append(resource.getResourceName());
message.style(STYLE).append(" (").append(format.format(contentLength));

long duration = System.currentTimeMillis() - resource.getTransferStartTime();
if (duration > 0L) {
double bytesPerSecond = contentLength / (duration / 1000.0);
Duration duration =
Duration.between(Instant.ofEpochMilli(resource.getTransferStartTime()), MonotonicClock.now());
if ((duration.getSeconds() | duration.getNano()) > 0) { // duration.isPositive()
long bytesPerSecond = Math.round(contentLength / (double) duration.toSeconds());
message.append(" at ");
format.format(message, (long) bytesPerSecond);
format.format(message, bytesPerSecond);
message.append("/s");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
*/
package org.apache.maven.cling.transfer;

import java.time.Duration;
import java.time.Instant;

import org.apache.maven.api.MonotonicClock;
import org.eclipse.aether.transfer.AbstractTransferListener;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.transfer.TransferEvent;
Expand Down Expand Up @@ -83,11 +87,12 @@ public void transferSucceeded(TransferEvent event) {
.append(" (");
format.format(message, contentLength);

long duration = System.currentTimeMillis() - resource.getTransferStartTime();
if (duration > 0L) {
double bytesPerSecond = contentLength / (duration / 1000.0);
Duration duration =
Duration.between(Instant.ofEpochMilli(resource.getTransferStartTime()), MonotonicClock.now());
if ((duration.getSeconds() | duration.getNano()) > 0) { // duration.isPositive()
long bytesPerSecond = Math.round(contentLength / (double) duration.toSeconds());
message.append(" at ");
format.format(message, (long) bytesPerSecond);
format.format(message, bytesPerSecond);
message.append("/s");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Locale;
import java.util.Properties;

Expand Down Expand Up @@ -105,23 +106,13 @@ public static String showVersionMinimal() {
* @return Readable build info
*/
public static String createMavenVersionString(Properties buildProperties) {
String timestamp = reduce(buildProperties.getProperty("timestamp"));
String version = reduce(buildProperties.getProperty(BUILD_VERSION_PROPERTY));
String rev = reduce(buildProperties.getProperty("buildNumber"));
String distributionName = reduce(buildProperties.getProperty("distributionName"));

String msg = distributionName + " ";
msg += (version != null ? version : "<version unknown>");
if (rev != null || timestamp != null) {
msg += " (";
msg += (rev != null ? rev : "");
if (timestamp != null && !timestamp.isEmpty()) {
String ts = formatTimestamp(Long.parseLong(timestamp));
msg += (rev != null ? "; " : "") + ts;
}
msg += ")";
}
return msg;
return distributionName + " "
+ (version != null ? version : "<version unknown>")
+ (rev != null ? " (" + rev + ")" : "");
}

private static String reduce(String s) {
Expand Down Expand Up @@ -169,35 +160,25 @@ public static void showError(Logger logger, String message, Throwable e, boolean
}
}

public static String formatTimestamp(long timestamp) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
return sdf.format(new Date(timestamp));
public static String formatTimestamp(TemporalAccessor instant) {
return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(instant);
}

public static String formatDuration(long duration) {
// CHECKSTYLE_OFF: MagicNumber
long ms = duration % 1000;
long s = (duration / ONE_SECOND) % 60;
long m = (duration / ONE_MINUTE) % 60;
long h = (duration / ONE_HOUR) % 24;
long d = duration / ONE_DAY;
// CHECKSTYLE_ON: MagicNumber

String format;
if (d > 0) {
// Length 11+ chars
format = "%d d %02d:%02d h";
} else if (h > 0) {
// Length 7 chars
format = "%2$02d:%3$02d h";
} else if (m > 0) {
// Length 9 chars
format = "%3$02d:%4$02d min";
public static String formatDuration(Duration duration) {
long days = duration.toDays();
long hours = duration.toHoursPart();
long minutes = duration.toMinutesPart();
long seconds = duration.toSecondsPart();
long millis = duration.toMillisPart();

if (days > 0) {
return String.format("%d d %02d:%02d h", days, hours, minutes);
} else if (hours > 0) {
return String.format("%02d:%02d h", hours, minutes);
} else if (minutes > 0) {
return String.format("%02d:%02d min", minutes, seconds);
} else {
// Length 7-8 chars
format = "%4$d.%5$03d s";
return String.format("%d.%03d s", seconds, millis);
}

return String.format(format, d, h, m, s, ms);
}
}
Loading

0 comments on commit 54ffc50

Please sign in to comment.