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()); + } +} diff --git a/jme3-core/src/main/java/com/jme3/light/LightList.java b/jme3-core/src/main/java/com/jme3/light/LightList.java index 2770f089f7..2089e2da55 100644 --- a/jme3-core/src/main/java/com/jme3/light/LightList.java +++ b/jme3-core/src/main/java/com/jme3/light/LightList.java @@ -51,6 +51,7 @@ public final class LightList implements Iterable, Savable, Cloneable, Jme private Light[] list, tlist; private float[] distToOwner; private int listSize; + private int tlistSize; private Spatial owner; private static final int DEFAULT_SIZE = 1; @@ -136,8 +137,9 @@ public void remove(int index) { return; } - for (int i = index; i < listSize; i++) { - list[i] = list[i+1]; + int copyLength = listSize - index; + if (copyLength > 0) { + System.arraycopy(list, index + 1, list, index, copyLength); } list[listSize] = null; } @@ -182,11 +184,11 @@ public void clear() { if (listSize == 0) return; - for (int i = 0; i < listSize; i++) - list[i] = null; + Arrays.fill(list, 0, listSize, null); if (tlist != null) - Arrays.fill(tlist, null); + Arrays.fill(tlist, 0, tlistSize, null); + tlistSize = 0; listSize = 0; } @@ -205,11 +207,15 @@ public void clear() { public void sort(boolean transformChanged) { if (listSize > 1) { // resize or populate our temporary array as necessary - if (tlist == null || tlist.length != list.length) { - tlist = list.clone(); + if (tlist == null || tlist.length < listSize) { + tlist = new Light[listSize]; } else { - System.arraycopy(list, 0, tlist, 0, list.length); + if (tlistSize > listSize) { + Arrays.fill(tlist, listSize, tlistSize, null); + } } + System.arraycopy(list, 0, tlist, 0, listSize); + tlistSize = listSize; if (transformChanged) { // check distance of each light @@ -249,31 +255,36 @@ public void update(LightList local, LightList parent, Predicate filter) { // using the arguments clear(); - while (list.length <= local.listSize) { + int requiredSize = local.listSize + (parent == null ? 0 : parent.listSize); + while (list.length < requiredSize) { doubleSize(); } - int localListSize = 0; - for(int i=0;i light != discard); + + Assertions.assertEquals(2, world.size()); + Assertions.assertSame(keep, world.get(0)); + Assertions.assertSame(parentLight, world.get(1)); + } +}