diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b6f9e5fdc..754e14d5a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ checkstyle = "13.3.0" jacoco = "0.8.12" +jmh = "1.37" lwjgl3 = "3.4.1" angle = "2026-05-09" saferalloc = "0.0.8" @@ -25,6 +26,8 @@ jbullet = "com.github.stephengold:jbullet:1.0.3" jinput = "net.java.jinput:jinput:2.0.9" jna = "net.java.dev.jna:jna:5.18.1" jnaerator-runtime = "com.nativelibs4java:jnaerator-runtime:0.12" +jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } +jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } junit-bom = "org.junit:junit-bom:5.13.4" junit4 = "junit:junit:4.13.2" junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } diff --git a/jme3-core/build.gradle b/jme3-core/build.gradle index 58ac362921..e022ca9456 100644 --- a/jme3-core/build.gradle +++ b/jme3-core/build.gradle @@ -13,12 +13,36 @@ sourceSets { System.setProperty "java.awt.headless", "true" } + benchmark { + java { + srcDir 'src/benchmark/java' + } + compileClasspath += sourceSets.main.runtimeClasspath + sourceSets.test.output + runtimeClasspath += output + compileClasspath + sourceSets.test.runtimeClasspath + } } dependencies { testRuntimeOnly project(':jme3-testdata') testImplementation project(':jme3-desktop') testRuntimeOnly project(':jme3-plugins') + benchmarkImplementation libs.jmh.core + benchmarkAnnotationProcessor libs.jmh.generator.annprocess +} + +tasks.register('jmh', JavaExec) { + dependsOn tasks.named('benchmarkClasses') + description = 'Run jme3-core JMH benchmarks. Pass JMH options with -PjmhArgs="...".' + group = 'verification' + classpath = sourceSets.benchmark.runtimeClasspath + mainClass = 'org.openjdk.jmh.Main' + + def rawArgs = providers.gradleProperty('jmhArgs').orElse('').map { it.trim() } + doFirst { + if (rawArgs.get()) { + args rawArgs.get().split(/\s+/) + } + } } task updateVersionPropertiesFile { diff --git a/jme3-core/src/benchmark/java/com/jme3/bounding/BoundingVolumeBenchmark.java b/jme3-core/src/benchmark/java/com/jme3/bounding/BoundingVolumeBenchmark.java new file mode 100644 index 0000000000..752a26b28e --- /dev/null +++ b/jme3-core/src/benchmark/java/com/jme3/bounding/BoundingVolumeBenchmark.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.bounding; + +import com.jme3.math.Vector3f; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(2) +@State(Scope.Thread) +public class BoundingVolumeBenchmark { + + @Param({"inside", "outside"}) + public String pointLocation; + + private BoundingBox box; + private BoundingSphere sphere; + private Vector3f point; + + @Setup(Level.Trial) + public void setupTrial() { + box = new BoundingBox(new Vector3f(3f, -2f, 7f), 5f, 9f, 13f); + sphere = new BoundingSphere(11f, new Vector3f(3f, -2f, 7f)); + if ("inside".equals(pointLocation)) { + point = new Vector3f(4f, 0f, 9f); + } else { + point = new Vector3f(39f, -31f, 45f); + } + } + + @Benchmark + public void boxDistanceToEdge(Blackhole blackhole) { + blackhole.consume(box.distanceToEdge(point)); + } + + @Benchmark + public void boxIntersectsPoint(Blackhole blackhole) { + blackhole.consume(box.intersects(point)); + } + + @Benchmark + public void sphereDistanceToEdge(Blackhole blackhole) { + blackhole.consume(sphere.distanceToEdge(point)); + } + + @Benchmark + public void sphereIntersectsPoint(Blackhole blackhole) { + blackhole.consume(sphere.intersects(point)); + } +} diff --git a/jme3-core/src/benchmark/java/com/jme3/light/LightListMutationBenchmark.java b/jme3-core/src/benchmark/java/com/jme3/light/LightListMutationBenchmark.java new file mode 100644 index 0000000000..016cddaed6 --- /dev/null +++ b/jme3-core/src/benchmark/java/com/jme3/light/LightListMutationBenchmark.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.light; + +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(2) +@State(Scope.Thread) +public class LightListMutationBenchmark { + + @Param({"8", "64", "256"}) + public int lightCount; + + private Geometry owner; + private Light[] lights; + private LightList list; + private LightList local; + private LightList parent; + + @Setup(Level.Trial) + public void setupTrial() { + owner = new Geometry("owner", new Mesh()); + lights = new Light[lightCount * 2]; + for (int i = 0; i < lights.length; i++) { + lights[i] = new PointLight(new Vector3f(i, i * 0.5f, -i)); + } + list = new LightList(owner); + local = new LightList(owner); + parent = new LightList(owner); + for (int i = 0; i < lightCount; i++) { + local.add(lights[i]); + parent.add(lights[lightCount + i]); + } + } + + @Setup(Level.Invocation) + public void setupInvocation() { + list.clear(); + for (int i = 0; i < lightCount; i++) { + list.add(lights[i]); + } + } + + @Benchmark + public void removeFromFront(Blackhole blackhole) { + list.remove(0); + blackhole.consume(list.size()); + } + + @Benchmark + public void removeFromMiddle(Blackhole blackhole) { + list.remove(lightCount >>> 1); + blackhole.consume(list.size()); + } + + @Benchmark + public void removeFromEnd(Blackhole blackhole) { + list.remove(lightCount - 1); + blackhole.consume(list.size()); + } + + @Benchmark + public void updateFromLocalAndParent(Blackhole blackhole) { + list.update(local, parent); + blackhole.consume(list.size()); + } +} diff --git a/jme3-core/src/benchmark/java/com/jme3/light/LightListSortBenchmark.java b/jme3-core/src/benchmark/java/com/jme3/light/LightListSortBenchmark.java new file mode 100644 index 0000000000..e1b7ac342b --- /dev/null +++ b/jme3-core/src/benchmark/java/com/jme3/light/LightListSortBenchmark.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.light; + +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(2) +@State(Scope.Thread) +public class LightListSortBenchmark { + + @Param({"8", "64", "256"}) + public int lightCount; + + @Param({"1", "8", "1024"}) + public int retainedCapacityMultiplier; + + private Geometry owner; + private Light[] lights; + private LightList list; + private int invocation; + + @Setup(Level.Trial) + public void setupTrial() { + owner = new Geometry("owner", new Mesh()); + owner.setLocalTranslation(3f, -7f, 11f); + owner.updateGeometricState(); + + lights = new Light[lightCount]; + Random random = new Random(0x51A7E5L + lightCount); + for (int i = 0; i < lightCount; i++) { + switch (i & 3) { + case 0: + lights[i] = new AmbientLight(); + break; + case 1: + lights[i] = new DirectionalLight(new Vector3f(1f, -1f, 0.25f).normalizeLocal()); + break; + default: + lights[i] = new PointLight(new Vector3f( + random.nextFloat() * 200f - 100f, + random.nextFloat() * 200f - 100f, + random.nextFloat() * 200f - 100f)); + break; + } + } + + list = new LightList(owner); + int retainedCapacity = Math.max(lightCount, lightCount * retainedCapacityMultiplier); + for (int i = 0; i < retainedCapacity; i++) { + list.add(lights[i % lightCount]); + } + list.clear(); + } + + @Setup(Level.Invocation) + public void setupInvocation() { + list.clear(); + int offset = (invocation++ & Integer.MAX_VALUE) % lightCount; + for (int i = 0; i < lightCount; i++) { + list.add(lights[(i + offset) % lightCount]); + } + } + + @Benchmark + public void sortTransformChanged(Blackhole blackhole) { + list.sort(true); + blackhole.consume(list.get(lightCount - 1)); + } +} diff --git a/jme3-core/src/benchmark/java/com/jme3/renderer/queue/GeometryListBenchmark.java b/jme3-core/src/benchmark/java/com/jme3/renderer/queue/GeometryListBenchmark.java new file mode 100644 index 0000000000..518b4cea2c --- /dev/null +++ b/jme3-core/src/benchmark/java/com/jme3/renderer/queue/GeometryListBenchmark.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.queue; + +import com.jme3.scene.Geometry; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Fork(2) +@State(Scope.Thread) +public class GeometryListBenchmark { + + @Param({"8", "64", "256", "1024"}) + public int geometryCount; + + private Geometry[] geometries; + private GeometryList list; + + @Setup(Level.Trial) + public void setupTrial() { + geometries = new Geometry[geometryCount]; + for (int i = 0; i < geometryCount; i++) { + geometries[i] = new Geometry("geom-" + i); + } + list = new GeometryList(new NullComparator()); + } + + @Setup(Level.Invocation) + public void setupInvocation() { + list.clear(); + for (Geometry geometry : geometries) { + list.add(geometry); + } + } + + @Benchmark + public void clear(Blackhole blackhole) { + list.clear(); + blackhole.consume(list.size()); + } +}