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
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
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 @@ -34,6 +33,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 @@ -109,7 +109,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
Loading