diff --git a/src/main/java/groovy/time/Timed.java b/src/main/java/groovy/time/Timed.java new file mode 100644 index 00000000000..e13a479c43a --- /dev/null +++ b/src/main/java/groovy/time/Timed.java @@ -0,0 +1,67 @@ +/* + * 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 groovy.time; + +import java.time.Duration; + +/** + * An immutable holder for the outcome of a timed execution: the value produced + * together with the elapsed time in nanoseconds. Instances are created by the + * {@code timed} extension method rather than being constructed directly. + *

+ * The elapsed time is measured with {@link System#nanoTime()}, which is + * monotonic and unaffected by wall-clock adjustments. Convenience views of the + * duration are available via {@link #getDuration()} and {@link #getMillis()}, + * which Groovy exposes as the {@code duration} and {@code millis} properties. + *

+ * def t = System.timed { (1..1_000).sum() }
+ * assert t.result == 500_500
+ * assert t.nanos >= 0
+ * assert t.duration instanceof java.time.Duration
+ * 
+ * + * @param the type of the timed result + * @param result the value returned by the timed code + * @param nanos the elapsed time in nanoseconds, as measured by {@link System#nanoTime()} + * @since 6.0.0 + */ +public record Timed(T result, long nanos) { + + /** + * Returns the elapsed time as a {@link java.time.Duration}. Exposed to Groovy + * as the read-only {@code duration} property. + * + * @return the elapsed time as a {@code Duration} + * @since 6.0.0 + */ + public Duration getDuration() { + return Duration.ofNanos(nanos); + } + + /** + * Returns the elapsed time in whole milliseconds, truncated toward zero. + * Exposed to Groovy as the read-only {@code millis} property. + * + * @return the elapsed time in milliseconds + * @since 6.0.0 + */ + public long getMillis() { + return nanos / 1_000_000; + } +} diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java index 52181bb3575..25c15c0ce66 100644 --- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java +++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java @@ -19,6 +19,7 @@ package org.codehaus.groovy.runtime; import groovy.lang.Closure; +import groovy.time.Timed; import org.codehaus.groovy.reflection.ReflectionUtils; import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation; @@ -209,6 +210,85 @@ public static void sleep(Object self, long milliseconds, Closure onInterrupt) { sleepImpl(milliseconds, onInterrupt); } + /** + * Executes the given closure and returns the elapsed time in nanoseconds. + *

+ * Timing uses {@link System#nanoTime()}, which is monotonic and unaffected by + * wall-clock adjustments. The closure is executed synchronously on the calling + * thread and any exception it throws propagates unchanged (the elapsed time is + * not reported in that case). Its result, if any, is discarded; use + * {@link #timed(System, Closure)} when the result is also needed. + *

+     * long elapsed = System.timedNanos { (1..1_000).sum() }
+     * assert elapsed >= 0
+     * 
+ * + * @param self placeholder variable used by Groovy categories; ignored for default static methods + * @param code the closure to execute and time + * @return the elapsed time in nanoseconds + * @see #timedMillis(System, Closure) + * @see #timed(System, Closure) + * @since 6.0.0 + */ + public static long timedNanos(System self, Closure code) { + long start = System.nanoTime(); + code.call(); + return System.nanoTime() - start; + } + + /** + * Executes the given closure and returns the elapsed time in whole milliseconds. + *

+ * This is a convenience for {@code timedNanos(code) / 1_000_000}. Timing uses + * {@link System#nanoTime()}, which is monotonic and unaffected by wall-clock + * adjustments. The closure is executed synchronously on the calling thread and + * any exception it throws propagates unchanged. Its result, if any, is + * discarded; use {@link #timed(System, Closure)} when the result is also needed. + *

+     * long elapsed = System.timedMillis { (1..1_000).sum() }
+     * assert elapsed >= 0
+     * 
+ * + * @param self placeholder variable used by Groovy categories; ignored for default static methods + * @param code the closure to execute and time + * @return the elapsed time in milliseconds, truncated toward zero + * @see #timedNanos(System, Closure) + * @see #timed(System, Closure) + * @since 6.0.0 + */ + public static long timedMillis(System self, Closure code) { + return timedNanos(self, code) / 1_000_000; + } + + /** + * Executes the given closure and returns a {@link Timed} capturing both the + * closure's result and the elapsed time in nanoseconds. + *

+ * Timing uses {@link System#nanoTime()}, which is monotonic and unaffected by + * wall-clock adjustments. The closure is executed synchronously on the calling + * thread and any exception it throws propagates unchanged. + *

+     * def t = System.timed { (1..1_000).sum() }
+     * assert t.result == 500_500
+     * assert t.nanos >= 0
+     * println "computed ${t.result} in ${t.millis}ms"
+     * 
+ * + * @param self placeholder variable used by Groovy categories; ignored for default static methods + * @param code the closure to execute and time + * @param the type of the closure's result + * @return a {@link Timed} holding the result and the elapsed nanoseconds + * @see #timedNanos(System, Closure) + * @see #timedMillis(System, Closure) + * @since 6.0.0 + */ + public static Timed timed(System self, Closure code) { + long start = System.nanoTime(); + T result = code.call(); + long nanos = System.nanoTime() - start; + return new Timed<>(result, nanos); + } + /** * Works exactly like ResourceBundle.getBundle(String). This is needed * because the java method depends on a particular stack configuration that diff --git a/src/test/groovy/org/codehaus/groovy/runtime/DefaultGroovyStaticMethodsTest.groovy b/src/test/groovy/org/codehaus/groovy/runtime/DefaultGroovyStaticMethodsTest.groovy index 81e4d0afb16..b6744819181 100644 --- a/src/test/groovy/org/codehaus/groovy/runtime/DefaultGroovyStaticMethodsTest.groovy +++ b/src/test/groovy/org/codehaus/groovy/runtime/DefaultGroovyStaticMethodsTest.groovy @@ -43,4 +43,41 @@ class DefaultGroovyStaticMethodsTest { void testAllThreads() { assert Thread.allThreads().stream().anyMatch(t -> 'Finalizer' == t.name) } + + @Test + void testTimedNanos() { + boolean ran = false + long elapsed = System.timedNanos { ran = true; (1..1000).sum() } + assert ran + assert elapsed >= 0 + } + + @Test + void testTimedMillis() { + boolean ran = false + long elapsed = System.timedMillis { ran = true; (1..1000).sum() } + assert ran + assert elapsed >= 0 + } + + @Test + void testTimed() { + def t = System.timed { (1..1000).sum() } + assert t.result == 500500 + assert t.nanos >= 0 + assert t.millis == t.nanos.intdiv(1_000_000) + assert t.duration == java.time.Duration.ofNanos(t.nanos) + } + + @Test + void testTimedPropagatesException() { + def ex = new IllegalStateException('boom') + def caught = null + try { + System.timed { throw ex } + } catch (IllegalStateException e) { + caught = e + } + assert caught.is(ex) + } }