src/jmh/java
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchGeogToGeom.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchGeogToGeom.java
new file mode 100644
index 00000000000..7e1a67804bb
--- /dev/null
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchGeogToGeom.java
@@ -0,0 +1,194 @@
+/*
+ * 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.sedona.bench;
+
+import com.google.common.geometry.S2LatLng;
+import com.google.common.geometry.S2Loop;
+import com.google.common.geometry.S2Point;
+import com.google.common.geometry.S2Polygon;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import one.profiler.AsyncProfiler;
+import org.apache.sedona.common.S2Geography.Geography;
+import org.apache.sedona.common.S2Geography.Geography.GeographyKind;
+import org.apache.sedona.common.S2Geography.MultiPolygonGeography;
+import org.apache.sedona.common.S2Geography.PolygonGeography;
+import org.apache.sedona.common.geography.Constructors;
+import org.locationtech.jts.geom.Geometry;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.BenchmarkParams;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.runner.IterationType;
+
+/**
+ * Benchmarks converting Sedona S2Geography -> JTS Geometry: - PolygonGeography -> JTS Polygon -
+ * MultiPolygonGeography -> JTS MultiPolygon
+ *
+ * Requirements: - JMH on classpath - async-profiler + ap-loader (if you want the profiler hook
+ * enabled)
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@State(Scope.Thread)
+public class BenchGeogToGeom {
+
+ // ============== Params ==============
+ // -------- Params --------
+ @Param({"1", "16", "256", "1024"})
+ public int numPolygons;
+
+ @Param({"4", "16", "256", "1024"})
+ public int verticesPerPolygon;
+
+ @Param({"XY", "XYZ"})
+ public String dimension;
+
+ /** Kept for parity with your other benches; not used by this conversion. */
+ @Param({"COMPACT"})
+ public String pointEncoding;
+
+ // ============== Reused data ==============
+ private List polygons; // synthetic S2 polygons
+ private Geography polygonGeog; // Geography (single polygon)
+ private Geography multiPolygonGeog; // Geography (multi polygon)
+
+ // ============== Setup ==============
+ @Setup(Level.Trial)
+ public void setup() {
+ this.polygons = buildPolygons(numPolygons, verticesPerPolygon);
+
+ // Single polygon geography (use the first polygon)
+ this.polygonGeog = new PolygonGeography(polygons.get(0));
+
+ // MultiPolygon geography (all polygons)
+ this.multiPolygonGeog = new MultiPolygonGeography(GeographyKind.MULTIPOLYGON, polygons);
+ }
+
+ // ============== Benchmarks ==============
+ /** Convert a single PolygonGeography to JTS Geometry. */
+ @Benchmark
+ public void geogPolygon_toJts(ProfilerHook ph, Blackhole bh) {
+ Geometry jts = Constructors.geogToGeometry(polygonGeog);
+ bh.consume(jts);
+ }
+
+ /** Convert a MultiPolygonGeography to JTS Geometry. */
+ @Benchmark
+ public void geogMultiPolygon_toJts(ProfilerHook ph, Blackhole bh) {
+ Geometry jts = Constructors.geogToGeometry(multiPolygonGeog);
+ bh.consume(jts);
+ }
+
+ // ============== Helpers ==============
+ /**
+ * Builds non‑overlapping S2Polygons by placing each shell on a small circle around a translated
+ * center. Each polygon consists of a single outer loop; no holes are added for simplicity.
+ */
+ public static List buildPolygons(int count, int verticesPerPolygon) {
+ List result = new ArrayList<>(Math.max(1, count));
+ double radiusDeg = 0.1; // small circle around the center
+
+ for (int j = 0; j < Math.max(1, count); j++) {
+ // spread centers to avoid overlaps and pathological degeneracies
+ double centerLat = -60.0 + j * 0.5;
+ double centerLng = -170.0 + j * 0.5;
+
+ List verts = new ArrayList<>(Math.max(3, verticesPerPolygon));
+ for (int i = 0; i < Math.max(3, verticesPerPolygon); i++) {
+ double angle = (2.0 * Math.PI * i) / Math.max(3, verticesPerPolygon);
+ double lat = centerLat + radiusDeg * Math.cos(angle);
+ double lng = centerLng + radiusDeg * Math.sin(angle);
+ verts.add(S2LatLng.fromDegrees(lat, lng).toPoint());
+ }
+
+ S2Loop shell = new S2Loop(verts);
+ shell.normalize(); // CCW for shells
+ result.add(new S2Polygon(shell));
+ }
+ return result;
+ }
+
+ // ============== Async‑profiler hook (optional) ==============
+ /** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
+ @State(Scope.Benchmark)
+ public static class ProfilerHook {
+ @Param({"cpu"})
+ public String event;
+
+ @Param({"jfr"})
+ public String format;
+
+ @Param({"1ms"})
+ public String interval;
+
+ private AsyncProfiler profiler;
+ private Path outDir;
+
+ @Setup(Level.Trial)
+ public void trial() throws Exception {
+ profiler = AsyncProfiler.getInstance();
+ outDir = Paths.get("profiles");
+ Files.createDirectories(outDir);
+ }
+
+ @Setup(Level.Iteration)
+ public void start(BenchmarkParams b, IterationParams it) throws Exception {
+ if (it.getType() != IterationType.MEASUREMENT) return;
+ String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
+ File out =
+ outDir
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
+ .toAbsolutePath()
+ .toFile();
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,all-user", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
+ } else {
+ profiler.execute("start," + common);
+ System.setProperty("ap.out", out.getAbsolutePath());
+ System.setProperty("ap.format", format);
+ }
+ }
+
+ @TearDown(Level.Iteration)
+ public void stop(IterationParams it) throws Exception {
+ if (it.getType() != IterationType.MEASUREMENT) return;
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("stop");
+ } else {
+ String file = System.getProperty("ap.out");
+ String fmt = System.getProperty("ap.format", "flamegraph");
+ profiler.execute(String.format("stop,file=%s,output=%s", file, fmt));
+ }
+ }
+ }
+}
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchGeomToGeog.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchGeomToGeog.java
new file mode 100644
index 00000000000..4a5cd887765
--- /dev/null
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchGeomToGeog.java
@@ -0,0 +1,192 @@
+/*
+ * 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.sedona.bench;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import one.profiler.AsyncProfiler;
+import org.apache.sedona.common.S2Geography.Geography;
+import org.apache.sedona.common.geography.Constructors;
+import org.locationtech.jts.geom.*;
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.infra.BenchmarkParams;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.runner.IterationType;
+
+/**
+ * Benchmarks converting JTS Geometry -> Sedona S2Geography: - Polygon -> PolygonGeography -
+ * MultiPolygon -> MultiPolygonGeography
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+@Fork(1)
+@State(Scope.Thread)
+public class BenchGeomToGeog {
+
+ // ============== Params ==============
+ /**
+ * Number of polygons in the MultiPolygon benchmark. Polygon benchmark uses the first polygon
+ * only.
+ */
+ // -------- Params --------
+ @Param({"1", "16", "256", "1024"})
+ public int numPolygons;
+
+ @Param({"4", "16", "256", "1024"})
+ public int verticesPerPolygon;
+
+ /** XY or XYZ coordinates in the generated JTS polygons. */
+ @Param({"XY", "XYZ"})
+ public String dimension;
+
+ // ============== Reused data ==============
+ private GeometryFactory gf;
+ private List polygons; // generated JTS polygons
+ private Polygon polygon; // first polygon
+ private MultiPolygon multiPolygon; // union as MultiPolygon
+
+ // ============== Setup ==============
+ @Setup(Level.Trial)
+ public void setup() {
+ // SRID=4326 for geographic degrees; PrecisionModel floating
+ gf = new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING), 4326);
+
+ polygons = buildPolygons(numPolygons, verticesPerPolygon, dimension, gf);
+ polygon = polygons.get(0);
+
+ Polygon[] arr = polygons.toArray(new Polygon[0]);
+ multiPolygon = gf.createMultiPolygon(arr);
+ }
+
+ // ============== Benchmarks ==============
+ @Benchmark
+ public void geomPolygon_toGeog(ProfilerHook ph, Blackhole bh) {
+ Geography g = Constructors.geomToGeography(polygon);
+ bh.consume(g);
+ }
+
+ @Benchmark
+ public void geomMultiPolygon_toGeog(ProfilerHook ph, Blackhole bh) {
+ Geography g = Constructors.geomToGeography(multiPolygon);
+ bh.consume(g);
+ }
+
+ // ============== Helpers ==============
+ /**
+ * Builds non-overlapping JTS Polygons in EPSG:4326. Each polygon is a simple closed ring sampled
+ * on a small circle (no holes).
+ */
+ public static List buildPolygons(
+ int count, int verticesPerPolygon, String dimension, GeometryFactory gf) {
+ int n = Math.max(1, count);
+ int v = Math.max(3, verticesPerPolygon);
+ boolean useZ = "XYZ".equalsIgnoreCase(dimension);
+
+ List result = new ArrayList<>(n);
+ double radiusDeg = 0.1; // small radius around each center
+
+ for (int j = 0; j < n; j++) {
+ double centerLat = -60.0 + j * 0.5;
+ double centerLng = -170.0 + j * 0.5;
+
+ Coordinate[] coords = new Coordinate[v + 1];
+ for (int i = 0; i < v; i++) {
+ double angle = (2.0 * Math.PI * i) / v;
+ double lat = centerLat + radiusDeg * Math.cos(angle);
+ double lng = centerLng + radiusDeg * Math.sin(angle);
+ double z = useZ ? 10.0 * Math.sin(angle) : Double.NaN; // smooth Z variation
+ coords[i] = useZ ? new Coordinate(lng, lat, z) : new Coordinate(lng, lat);
+ }
+ // close the ring
+ coords[v] = new Coordinate(coords[0]);
+
+ LinearRing shell = gf.createLinearRing(coords);
+ Polygon poly = gf.createPolygon(shell, null);
+ result.add(poly);
+ }
+ return result;
+ }
+
+ // ============== Async‑profiler hook (optional) ==============
+ /** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
+ @State(Scope.Benchmark)
+ public static class ProfilerHook {
+ @Param({"cpu"})
+ public String event;
+
+ @Param({"jfr"})
+ public String format;
+
+ @Param({"1ms"})
+ public String interval;
+
+ private AsyncProfiler profiler;
+ private Path outDir;
+
+ @Setup(Level.Trial)
+ public void trial() throws Exception {
+ profiler = AsyncProfiler.getInstance();
+ outDir = Paths.get("profiles");
+ Files.createDirectories(outDir);
+ }
+
+ @Setup(Level.Iteration)
+ public void start(BenchmarkParams b, IterationParams it) throws Exception {
+ if (it.getType() != IterationType.MEASUREMENT) return;
+ String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
+ File out =
+ outDir
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
+ .toAbsolutePath()
+ .toFile();
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,all-user", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
+ } else {
+ profiler.execute("start," + common);
+ System.setProperty("ap.out", out.getAbsolutePath());
+ System.setProperty("ap.format", format);
+ }
+ }
+
+ @TearDown(Level.Iteration)
+ public void stop(IterationParams it) throws Exception {
+ if (it.getType() != IterationType.MEASUREMENT) return;
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("stop");
+ } else {
+ String file = System.getProperty("ap.out");
+ String fmt = System.getProperty("ap.format", "flamegraph");
+ profiler.execute(String.format("stop,file=%s,output=%s", file, fmt));
+ }
+ }
+ }
+}
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java
index 221a060bfa9..e6e57ded144 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java
@@ -20,13 +20,14 @@
import java.io.File;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.*;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import one.profiler.AsyncProfiler;
-import one.profiler.AsyncProfilerLoader;
import org.apache.sedona.common.S2Geography.Geography;
import org.apache.sedona.common.S2Geography.WKBReader;
import org.apache.sedona.common.S2Geography.WKBWriter;
@@ -34,9 +35,7 @@
import org.locationtech.jts.io.ByteOrderValues;
import org.locationtech.jts.io.ParseException;
import org.openjdk.jmh.annotations.*;
-import org.openjdk.jmh.infra.BenchmarkParams;
-import org.openjdk.jmh.infra.Blackhole;
-import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.infra.*;
import org.openjdk.jmh.runner.IterationType;
@BenchmarkMode(Mode.AverageTime)
@@ -46,145 +45,220 @@
@Fork(1)
@State(Scope.Thread)
public class BenchPointWKB {
- /** Limit dimension to XY / XYZ only */
+
@Param({"XY", "XYZ"})
public String dim;
- /** number of points in MULTIPOINT */
- @Param({"1", "16", "256", "4096", "65536"})
- public int nPoints;
-
- /** WKB endianness */
@Param({"LE", "BE"})
public String endianness;
- // Fixtures prepared once per trial
- private String wkt;
- private Geography geo;
- private byte[] wkbLE;
- private byte[] wkbBE;
+ @Param({"1", "16", "256", "1024", "4096", "65536"})
+ public int nPoints;
+
+ // Fixtures
+ private String wktPoint, wktMulti;
+ private Geography geoPoint, geoMulti;
+
+ // use writer for write benches (to benchmark it)
+ private byte[] wkbPointLE, wkbPointBE;
+
+ // use hand-built payloads for read benches (so reader always accepts them)
+ private byte[] wkbReadPointLE, wkbReadPointBE;
+ private byte[] wkbReadMultiLE, wkbReadMultiBE;
@Setup(Level.Trial)
- public void setup() throws ParseException, IOException, org.locationtech.jts.io.ParseException {
- wkt = buildMultiPointWKT(nPoints, dim);
+ public void setup() throws ParseException, IOException {
+ wktPoint = buildPointWKT(dim);
+ wktMulti = buildMultiPointWKT(nPoints, dim);
- // Precompute Geography for writer bench
WKTReader wktReader = new WKTReader();
- geo = wktReader.read(wkt);
+ geoPoint = wktReader.read(wktPoint);
+ geoMulti = wktReader.read(wktMulti);
- // Precompute WKB for reader bench
- int outDims = ("XY".equals(dim) ? 2 : 3);
+ // Precompute writer outputs (so write benches time only serialization)
+ int outDims = "XY".equals(dim) ? 2 : 3;
WKBWriter le = new WKBWriter(outDims, ByteOrderValues.LITTLE_ENDIAN);
WKBWriter be = new WKBWriter(outDims, ByteOrderValues.BIG_ENDIAN);
- wkbLE = le.write(geo);
- wkbBE = be.write(geo);
+ wkbPointLE = le.write(geoPoint);
+ wkbPointBE = be.write(geoPoint);
+
+ // Precompute READ payloads with explicit layout the reader expects
+ boolean isXYZ = "XYZ".equals(dim);
+ wkbReadPointLE = buildPointWKB(/*little=*/ true, isXYZ, 0);
+ wkbReadPointBE = buildPointWKB(/*little=*/ false, isXYZ, 0);
+ wkbReadMultiLE = buildMultiPointWKB(/*little=*/ true, isXYZ, nPoints);
+ wkbReadMultiBE = buildMultiPointWKB(/*little=*/ false, isXYZ, nPoints);
}
- /** WKT → Geography (parse only) */
+ // ---------------- WRITE (Geography -> WKB) ----------------
+
@Benchmark
- public void wkt_read(Blackhole bh) throws ParseException, org.locationtech.jts.io.ParseException {
- WKTReader reader = new WKTReader();
- Geography g = reader.read(wkt);
- bh.consume(g);
- bh.consume(g.numShapes());
+ public double wkb_write_point(Blackhole bh, BenchPolygonWKB.ProfilerHook ph) throws IOException {
+ return write(geoPoint, bh);
}
- /** Geography → WKB (serialize only) */
@Benchmark
- public double wkb_write(Blackhole bh, ProfilerHook ph) throws IOException {
- // choose output dimensions (2 = XY, 3 = XYZ)
- int outDims = ("XY".equals(dim) ? 2 : 3);
+ public double wkb_write_multipoint(Blackhole bh, BenchPolygonWKB.ProfilerHook ph)
+ throws IOException {
+ return write(geoMulti, bh);
+ }
+
+ private double write(Geography g, Blackhole bh) throws IOException {
+ int outDims = "XY".equals(dim) ? 2 : 3;
int order =
"LE".equals(endianness) ? ByteOrderValues.LITTLE_ENDIAN : ByteOrderValues.BIG_ENDIAN;
-
- // do the actual write
WKBWriter writer = new WKBWriter(outDims, order);
- byte[] out = writer.write(geo);
+ byte[] out = writer.write(g);
long sum = 0;
- for (byte b : out) {
- sum += (b & 0xFF);
- }
+ for (byte b : out) sum += (b & 0xFF);
bh.consume(out);
return (double) sum;
}
- /** WKB → Geography (deserialize only) */
+ // ---------------- READ (WKB -> Geography) ----------------
+
+ @Benchmark
+ public void wkb_read_point(Blackhole bh, BenchPointWKB.ProfilerHook ph)
+ throws IOException, ParseException {
+ byte[] src = "LE".equals(endianness) ? wkbReadPointLE : wkbReadPointBE;
+ readInto(src, bh);
+ }
+
@Benchmark
- public void wkb_read(Blackhole bh, ProfilerHook ph)
- throws IOException, org.locationtech.jts.io.ParseException {
+ public void wkb_read_multipoint(Blackhole bh, BenchPointWKB.ProfilerHook ph)
+ throws IOException, ParseException {
+ byte[] src = "LE".equals(endianness) ? wkbReadMultiLE : wkbReadMultiBE;
+ readInto(src, bh);
+ }
+
+ private void readInto(byte[] src, Blackhole bh) throws IOException, ParseException {
WKBReader reader = new WKBReader();
- byte[] src = "LE".equals(endianness) ? wkbLE : wkbBE;
Geography g = reader.read(src);
bh.consume(g);
bh.consume(g.numShapes());
}
- // ---------- helpers ----------
+ // ---------------- Hand-built WKB for READ benches ----------------
- private static String buildMultiPointWKT(int n, String dim) {
- StringBuilder sb = new StringBuilder();
- // XY -> "MULTIPOINT"; XYZ -> "MULTIPOINT Z"
- sb.append("MULTIPOINT").append("XYZ".equals(dim) ? " Z" : "").append(" (");
+ private static byte[] buildPointWKB(boolean little, boolean xyz, int index) {
+ // (endianness)1 + (type)4 + coords
+ int type = xyz ? 1001 : 1;
+ int doubles = xyz ? 3 : 2;
+ int len = 1 + 4 + 8 * doubles; // No coordinate count for a single Point
+
+ ByteBuffer bb =
+ ByteBuffer.allocate(len).order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ bb.put(little ? (byte) 1 : (byte) 0);
+ bb.putInt(type);
+ double[] c = pointCoord(index, xyz);
+ bb.putDouble(c[0]);
+ bb.putDouble(c[1]);
+ if (xyz) bb.putDouble(c[2]);
+ return bb.array();
+ }
+
+ private static byte[] buildMultiPointWKB(boolean little, boolean xyz, int n) {
+ int pointType = xyz ? 1001 : 1;
+ int multiType = xyz ? 1004 : 4;
+ int doubles = xyz ? 3 : 2;
+
+ // header: endian(1) + type(4) + count(4)
+ int header = 1 + 4 + 4;
+ // each point: endian(1) + type(4) + doubles*8
+ int perPoint = 1 + 4 + 8 * doubles;
+
+ ByteBuffer bb =
+ ByteBuffer.allocate(header + n * perPoint)
+ .order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+
+ // Write MultiPoint header
+ bb.put(little ? (byte) 1 : (byte) 0);
+ bb.putInt(multiType);
+ bb.putInt(n);
+
+ // THE FIX: Write each Point as a full WKB geometry, as the reader expects
for (int i = 0; i < n; i++) {
- if (i > 0) sb.append(", ");
- sb.append('(').append(coord(i + 1, dim)).append(')');
+ bb.put(little ? (byte) 1 : (byte) 0); // inner endian
+ bb.putInt(pointType); // inner type (POINT)
+ double[] c = pointCoord(i, xyz);
+ bb.putDouble(c[0]);
+ bb.putDouble(c[1]);
+ if (xyz) bb.putDouble(c[2]);
}
- sb.append(')');
- return sb.toString();
+ return bb.array();
+ }
+
+ private static double[] pointCoord(int i, boolean xyz) {
+ double x = 10.0 + i * 0.001;
+ double y = 20.0 + i * 0.002;
+ if (!xyz) return new double[] {x, y};
+ double z = (i % 13) + 0.125;
+ return new double[] {x, y, z};
+ }
+
+ // ---------------- WKT helpers (for writer fixtures) ----------------
+
+ private static String buildPointWKT(String dim) {
+ return "POINT" + ("XYZ".equals(dim) ? " Z" : "") + " (" + coord(0, dim) + ")";
+ }
+
+ private static String buildMultiPointWKT(int n, String dim) {
+ if (n <= 1) return buildPointWKT(dim);
+ String dimToken = "XYZ".equals(dim) ? " Z" : "";
+ String pts =
+ IntStream.range(0, n).mapToObj(i -> coord(i, dim)).collect(Collectors.joining(", "));
+ return "MULTIPOINT" + dimToken + " (" + pts + ")";
}
- // Emit small non-zero Z in XYZ to ensure real parsing work
private static String coord(int i, String dim) {
- double x = i, y = i;
- if ("XY".equals(dim)) {
- return String.format(Locale.ROOT, "%.6f %.6f", x, y);
- } else { // XYZ
- double z = (i % 11) + 0.25;
- return String.format(Locale.ROOT, "%.6f %.6f %.6f", x, y, z);
- }
+ double x = 10.0 + i * 0.001;
+ double y = 20.0 + i * 0.002;
+ if ("XY".equals(dim)) return String.format(Locale.ROOT, "%.6f %.6f", x, y);
+ double z = (i % 13) + 0.125;
+ return String.format(Locale.ROOT, "%.6f %.6f %.6f", x, y, z);
}
- // =====================================================================
- // == Async-profiler hook (per-iteration, not inside the benchmark) ==
- // =====================================================================
+ // -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
public static class ProfilerHook {
@Param({"cpu"})
- public String event; // cpu | alloc | wall | lock
+ public String event;
@Param({"jfr"})
- public String format; // jfr | flamegraph | collapsed
+ public String format;
@Param({"1ms"})
- public String interval; // e.g., 1ms (for CPU), ignored by alloc
+ public String interval;
private AsyncProfiler profiler;
private Path outDir;
@Setup(Level.Trial)
public void trial() throws Exception {
- profiler = AsyncProfilerLoader.load();
+ profiler = AsyncProfiler.getInstance();
outDir = Paths.get("profiles");
Files.createDirectories(outDir);
}
@Setup(Level.Iteration)
public void start(BenchmarkParams b, IterationParams it) throws Exception {
- if (it.getType() != IterationType.MEASUREMENT) return; // skip warmups
- // Make a readable, unique file per iteration
+ if (it.getType() != IterationType.MEASUREMENT) return;
String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
File out =
outDir
- .resolve(base + (format.equals("jfr") ? ".jfr" : ".html"))
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
.toAbsolutePath()
.toFile();
- if ("jfr".equals(format)) {
- profiler.execute(
- String.format("start,jfr,event=%s,interval=%s,file=%s", event, interval, out));
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
} else {
- // For non-JFR, start now; we'll set file/output on stop
- profiler.execute(String.format("start,event=%s,interval=%s", event, interval));
+ profiler.execute("start," + common);
System.setProperty("ap.out", out.getAbsolutePath());
System.setProperty("ap.format", format);
}
@@ -193,7 +267,7 @@ public void start(BenchmarkParams b, IterationParams it) throws Exception {
@TearDown(Level.Iteration)
public void stop(IterationParams it) throws Exception {
if (it.getType() != IterationType.MEASUREMENT) return;
- if ("jfr".equals(format)) {
+ if ("jfr".equalsIgnoreCase(format)) {
profiler.execute("stop");
} else {
String file = System.getProperty("ap.out");
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java
index a108105e521..1ebbecbc4c0 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java
@@ -20,15 +20,14 @@
import java.io.File;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.*;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import one.profiler.AsyncProfiler;
-import one.profiler.AsyncProfilerLoader;
import org.apache.sedona.common.S2Geography.Geography;
import org.apache.sedona.common.S2Geography.WKBReader;
import org.apache.sedona.common.S2Geography.WKBWriter;
@@ -36,9 +35,7 @@
import org.locationtech.jts.io.ByteOrderValues;
import org.locationtech.jts.io.ParseException;
import org.openjdk.jmh.annotations.*;
-import org.openjdk.jmh.infra.BenchmarkParams;
-import org.openjdk.jmh.infra.Blackhole;
-import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.infra.*;
import org.openjdk.jmh.runner.IterationType;
@BenchmarkMode(Mode.AverageTime)
@@ -48,168 +45,249 @@
@Fork(1)
@State(Scope.Thread)
public class BenchPolygonWKB {
- /** Limit dimension to XY / XYZ only */
+ /** XY or XYZ */
@Param({"XY", "XYZ"})
public String dim;
/** number of polygons in MULTIPOLYGON */
- @Param({"1", "1", "1", "1", "16", "256", "1028"})
+ @Param({"1", "16", "256"})
public int nPolygons;
- /** number of vertices per polygon ring (must be >= 4) */
- @Param({"4", "16", "256", "1028", "1028", "1028"})
+ /** vertices per polygon outer ring (>=4; last coord auto‑closed) */
+ @Param({"4", "16", "256"})
public int nVerticesPerRing;
/** WKB endianness */
@Param({"LE", "BE"})
public String endianness;
- // Fixtures prepared once per trial
- private String wkt;
- private Geography geo;
- private byte[] wkbLE;
- private byte[] wkbBE;
+ // ---- Fixtures (prepared once per trial) ----
+ private String wktPolygon;
+ private String wktMultiPolygon;
+ private Geography geoPolygon;
+ private Geography geoMultiPolygon;
+ // Payloads for READ benchmarks are now hand-built
+ private byte[] wkbReadPolygonLE, wkbReadPolygonBE;
+ private byte[] wkbReadMultiLE, wkbReadMultiBE;
@Setup(Level.Trial)
- public void setup() throws ParseException, IOException, org.locationtech.jts.io.ParseException {
- wkt = buildMultiPolygonWKT(nPolygons, nVerticesPerRing, dim);
+ public void setup() throws ParseException, IOException {
+ int v = Math.max(4, nVerticesPerRing);
+ int p = Math.max(1, nPolygons);
- // Precompute Geography for writer bench
+ // WKT and Geography objects are still needed for the WRITE benchmarks
+ wktPolygon = buildPolygonWKT(v, dim, 0);
+ wktMultiPolygon = buildMultiPolygonWKT(p, v, dim);
WKTReader wktReader = new WKTReader();
- geo = wktReader.read(wkt);
+ geoPolygon = wktReader.read(wktPolygon);
+ geoMultiPolygon = wktReader.read(wktMultiPolygon);
- // Precompute WKB for reader bench
- int outDims = ("XY".equals(dim) ? 2 : 3);
- WKBWriter le = new WKBWriter(outDims, ByteOrderValues.LITTLE_ENDIAN);
- WKBWriter be = new WKBWriter(outDims, ByteOrderValues.BIG_ENDIAN);
- wkbLE = le.write(geo);
- wkbBE = be.write(geo);
+ // THE FIX: Build custom WKB for the READ benchmarks
+ boolean isXYZ = "XYZ".equals(dim);
+ wkbReadPolygonLE = buildPolygonWKB(true, isXYZ, v, 0);
+ wkbReadPolygonBE = buildPolygonWKB(false, isXYZ, v, 0);
+ wkbReadMultiLE = buildMultiPolygonWKB(true, isXYZ, p, v);
+ wkbReadMultiBE = buildMultiPolygonWKB(false, isXYZ, p, v);
}
- /** WKT → Geography (parse only) */
+ // ---- WRITE (Geography -> WKB) ----
@Benchmark
- public void wkt_read(Blackhole bh) throws ParseException, org.locationtech.jts.io.ParseException {
- WKTReader reader = new WKTReader();
- Geography g = reader.read(wkt);
- bh.consume(g);
- bh.consume(g.numShapes());
+ public double wkb_write_polygon(Blackhole bh, BenchPolygonWKB.ProfilerHook ph)
+ throws IOException {
+ return write(geoPolygon, bh);
}
- /** Geography → WKB (serialize only) */
@Benchmark
- public double wkb_write(Blackhole bh, ProfilerHook ph) throws IOException {
- // choose output dimensions (2 = XY, 3 = XYZ)
+ public double wkb_write_multipolygon(Blackhole bh, BenchPolygonWKB.ProfilerHook ph)
+ throws IOException {
+ return write(geoMultiPolygon, bh);
+ }
+
+ private double write(Geography g, Blackhole bh) throws IOException {
int outDims = ("XY".equals(dim) ? 2 : 3);
int order =
"LE".equals(endianness) ? ByteOrderValues.LITTLE_ENDIAN : ByteOrderValues.BIG_ENDIAN;
-
- // do the actual write
WKBWriter writer = new WKBWriter(outDims, order);
- byte[] out = writer.write(geo);
+ byte[] out = writer.write(g);
long sum = 0;
- for (byte b : out) {
- sum += (b & 0xFF);
- }
+ for (byte b : out) sum += (b & 0xFF); // prevent DCE
bh.consume(out);
return (double) sum;
}
- /** WKB → Geography (deserialize only) */
+ // ---- READ (WKB -> Geography) ----
+ @Benchmark
+ public void wkb_read_polygon(Blackhole bh, BenchPolygonWKB.ProfilerHook ph)
+ throws IOException, ParseException {
+ read(("LE".equals(endianness) ? wkbReadPolygonLE : wkbReadPolygonBE), bh);
+ }
+
@Benchmark
- public void wkb_read(Blackhole bh, ProfilerHook ph)
- throws IOException, org.locationtech.jts.io.ParseException {
+ public void wkb_read_multipolygon(Blackhole bh, BenchPolygonWKB.ProfilerHook ph)
+ throws IOException, ParseException {
+ read(("LE".equals(endianness) ? wkbReadMultiLE : wkbReadMultiBE), bh);
+ }
+
+ private void read(byte[] src, Blackhole bh) throws IOException, ParseException {
WKBReader reader = new WKBReader();
- byte[] src = "LE".equals(endianness) ? wkbLE : wkbBE;
Geography g = reader.read(src);
bh.consume(g);
bh.consume(g.numShapes());
}
- // ---------- helpers ----------
+ // ---- Hand-built WKB for READ benches ----
+
+ private static byte[] buildPolygonWKB(boolean little, boolean xyz, int nVertices, int polyIndex) {
+ int type = xyz ? 1003 : 3; // Polygon type
+ int doubles = xyz ? 3 : 2;
+ // endian, type, num_rings, num_coords, coords
+ int len = 1 + 4 + 4 + 4 + (8 * doubles * nVertices);
+
+ ByteBuffer bb =
+ ByteBuffer.allocate(len).order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ bb.put(little ? (byte) 1 : (byte) 0);
+ bb.putInt(type);
+ bb.putInt(1); // num_rings = 1 (simple polygon with no holes)
+ bb.putInt(nVertices);
+
+ // Build coordinates for the ring
+ double cx = -170.0 + polyIndex * 0.5;
+ double cy = -60.0 + polyIndex * 0.5;
+ double rDeg = 0.1 + (polyIndex % 5) * 0.02;
+
+ for (int i = 0; i < nVertices - 1; i++) {
+ double ang = (2.0 * Math.PI * i) / (nVertices - 1);
+ double x = cx + rDeg * Math.cos(ang);
+ double y = cy + rDeg * Math.sin(ang);
+ putCoord(bb, x, y, xyz, i);
+ }
+ // Close the ring
+ putCoord(bb, cx + rDeg, cy, xyz, 0);
+
+ return bb.array();
+ }
+
+ private static byte[] buildMultiPolygonWKB(
+ boolean little, boolean xyz, int nPolys, int nVertices) {
+ int multiType = xyz ? 1006 : 6; // MultiPolygon type
+ int header = 1 + 4 + 4; // endian, type, num_polygons
+ // Get the size of a single polygon WKB to calculate total length
+ byte[] singlePoly = buildPolygonWKB(little, xyz, nVertices, 0);
+ int len = header + (nPolys * singlePoly.length);
- private static String buildMultiPolygonWKT(int numPolygons, int numVerticesPerRing, String dim) {
- if (numVerticesPerRing < 4) {
- throw new IllegalArgumentException(
- "A polygon ring must have at least 4 vertices to be closed.");
+ ByteBuffer bb =
+ ByteBuffer.allocate(len).order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ bb.put(little ? (byte) 1 : (byte) 0);
+ bb.putInt(multiType);
+ bb.putInt(nPolys);
+ for (int i = 0; i < nPolys; i++) {
+ // The reader expects each polygon to be a full, self-contained WKB geometry
+ bb.put(buildPolygonWKB(little, xyz, nVertices, i));
}
+ return bb.array();
+ }
+
+ private static void putCoord(ByteBuffer bb, double x, double y, boolean xyz, int i) {
+ bb.putDouble(x);
+ bb.putDouble(y);
+ if (xyz) {
+ double z = (i % 19) + 0.5;
+ bb.putDouble(z);
+ }
+ }
+
+ // ---- Helpers: WKT generators ----
+
+ private static String buildPolygonWKT(int verticesPerRing, String dim, int polyIndex) {
String dimToken = "XYZ".equals(dim) ? " Z" : "";
- String polygons =
- IntStream.range(0, numPolygons)
+ String ring = buildRing(verticesPerRing, dim, polyIndex);
+ return "POLYGON" + dimToken + " ((" + ring + "))";
+ }
+
+ private static String buildMultiPolygonWKT(int p, int verticesPerRing, String dim) {
+ String dimToken = "XYZ".equals(dim) ? " Z" : "";
+ String polys =
+ IntStream.range(0, p)
+ .mapToObj(i -> "((" + buildRing(verticesPerRing, dim, i) + "))")
+ .collect(Collectors.joining(", "));
+ return "MULTIPOLYGON" + dimToken + " (" + polys + ")";
+ }
+
+ private static String buildRing(int v, String dim, int polyIndex) {
+ if (v < 4) throw new IllegalArgumentException("Polygon ring must have >= 4 vertices");
+ double cx = -170.0 + polyIndex * 0.5;
+ double cy = -60.0 + polyIndex * 0.5;
+ double rDeg = 0.1 + (polyIndex % 5) * 0.02;
+
+ String vertices =
+ IntStream.range(0, v - 1)
.mapToObj(
i -> {
- // Generate vertices for the outer ring
- int baseCoordIndex = i * numVerticesPerRing;
- String firstCoord = coord(baseCoordIndex, dim);
- String vertices =
- IntStream.range(0, numVerticesPerRing - 1)
- .mapToObj(j -> coord(baseCoordIndex + j, dim))
- .collect(Collectors.joining(", "));
- // A ring must be closed, so the last coordinate is the same as the first
- String ring = vertices + ", " + firstCoord;
- // For simplicity, we only generate polygons with one outer ring and no holes.
- return "((" + ring + "))";
+ double ang = (2.0 * Math.PI * i) / (v - 1);
+ double x = cx + rDeg * Math.cos(ang);
+ double y = cy + rDeg * Math.sin(ang);
+ return formatCoord(x, y, dim, i);
})
.collect(Collectors.joining(", "));
- return "MULTIPOLYGON" + dimToken + " (" + polygons + ")";
- }
- // Emit small non-zero Z in XYZ to ensure real parsing work
- private static String coord(int i, String dim) {
- // Generate non-overlapping convex polygons for simplicity
- double angle = 2 * Math.PI * (i % 100) / 100;
- double radius = 10 + (i / 100);
- double x = radius * Math.cos(angle);
- double y = radius * Math.sin(angle);
+ String first = formatCoord(cx + rDeg, cy, dim, 0);
+ return vertices + ", " + first;
+ }
+ private static String formatCoord(double x, double y, String dim, int i) {
if ("XY".equals(dim)) {
return String.format(Locale.ROOT, "%.6f %.6f", x, y);
- } else { // XYZ
- double z = (i % 11) + 0.25;
+ } else {
+ double z = (i % 19) + 0.5;
return String.format(Locale.ROOT, "%.6f %.6f %.6f", x, y, z);
}
}
+
// =====================================================================
- // == Async-profiler hook (per-iteration, not inside the benchmark) ==
+ // == Async-profiler hook (runs inside the fork) ==
// =====================================================================
+ // -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
public static class ProfilerHook {
@Param({"cpu"})
- public String event; // cpu | alloc | wall | lock
+ public String event;
@Param({"jfr"})
- public String format; // jfr | flamegraph | collapsed
+ public String format;
@Param({"1ms"})
- public String interval; // e.g., 1ms (for CPU), ignored by alloc
+ public String interval;
private AsyncProfiler profiler;
private Path outDir;
@Setup(Level.Trial)
public void trial() throws Exception {
- profiler = AsyncProfilerLoader.load();
+ profiler = AsyncProfiler.getInstance();
outDir = Paths.get("profiles");
Files.createDirectories(outDir);
}
@Setup(Level.Iteration)
public void start(BenchmarkParams b, IterationParams it) throws Exception {
- if (it.getType() != IterationType.MEASUREMENT) return; // skip warmups
- // Make a readable, unique file per iteration
+ if (it.getType() != IterationType.MEASUREMENT) return;
String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
File out =
outDir
- .resolve(base + (format.equals("jfr") ? ".jfr" : ".html"))
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
.toAbsolutePath()
.toFile();
- if ("jfr".equals(format)) {
- profiler.execute(
- String.format("start,jfr,event=%s,interval=%s,file=%s", event, interval, out));
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
} else {
- // For non-JFR, start now; we'll set file/output on stop
- profiler.execute(String.format("start,event=%s,interval=%s", event, interval));
+ profiler.execute("start," + common);
System.setProperty("ap.out", out.getAbsolutePath());
System.setProperty("ap.format", format);
}
@@ -218,7 +296,7 @@ public void start(BenchmarkParams b, IterationParams it) throws Exception {
@TearDown(Level.Iteration)
public void stop(IterationParams it) throws Exception {
if (it.getType() != IterationType.MEASUREMENT) return;
- if ("jfr".equals(format)) {
+ if ("jfr".equalsIgnoreCase(format)) {
profiler.execute("stop");
} else {
String file = System.getProperty("ap.out");
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolylineWKB.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolylineWKB.java
index 1d459765660..230d04b63dd 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolylineWKB.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolylineWKB.java
@@ -20,15 +20,14 @@
import java.io.File;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.*;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import one.profiler.AsyncProfiler;
-import one.profiler.AsyncProfilerLoader;
import org.apache.sedona.common.S2Geography.Geography;
import org.apache.sedona.common.S2Geography.WKBReader;
import org.apache.sedona.common.S2Geography.WKBWriter;
@@ -36,9 +35,7 @@
import org.locationtech.jts.io.ByteOrderValues;
import org.locationtech.jts.io.ParseException;
import org.openjdk.jmh.annotations.*;
-import org.openjdk.jmh.infra.BenchmarkParams;
-import org.openjdk.jmh.infra.Blackhole;
-import org.openjdk.jmh.infra.IterationParams;
+import org.openjdk.jmh.infra.*;
import org.openjdk.jmh.runner.IterationType;
@BenchmarkMode(Mode.AverageTime)
@@ -48,84 +45,146 @@
@Fork(1)
@State(Scope.Thread)
public class BenchPolylineWKB {
- /** Limit dimension to XY / XYZ only */
+
+ /** XY or XYZ */
@Param({"XY", "XYZ"})
public String dim;
/** number of lines in MULTILINESTRING */
- @Param({"1", "1", "1", "1", "16", "256", "1028"})
+ @Param({"1", "16", "256", "1024"})
public int nLines;
- /** number of vertices per line */
- @Param({"2", "16", "256", "1028", "1028", "1028", "1028"})
+ /** number of vertices per line (min 2) */
+ @Param({"2", "16", "256", "1024"})
public int nVerticesPerLine;
/** WKB endianness */
@Param({"LE", "BE"})
public String endianness;
- // Fixtures prepared once per trial
- private String wkt;
- private Geography geo;
- private byte[] wkbLE;
- private byte[] wkbBE;
+ // ---- Fixtures (prepared once per trial) ----
+ private String wktLine;
+ private String wktMulti;
+ private Geography geoLine;
+ private Geography geoMulti;
+ private byte[] wkbReadSingleLE, wkbReadSingleBE;
+ private byte[] wkbReadMultiLE, wkbReadMultiBE;
@Setup(Level.Trial)
- public void setup() throws ParseException, IOException, org.locationtech.jts.io.ParseException {
- wkt = buildMultiLineWKT(nLines, nVerticesPerLine, dim);
+ public void setup() throws ParseException, IOException {
+ wktLine = buildLineWKT(Math.max(2, nVerticesPerLine), dim);
+ wktMulti = buildMultiLineWKT(Math.max(1, nLines), Math.max(2, nVerticesPerLine), dim);
- // Precompute Geography for writer bench
WKTReader wktReader = new WKTReader();
- geo = wktReader.read(wkt);
+ geoLine = wktReader.read(wktLine);
+ geoMulti = wktReader.read(wktMulti);
- // Precompute WKB for reader bench
- int outDims = ("XY".equals(dim) ? 2 : 3);
- WKBWriter le = new WKBWriter(outDims, ByteOrderValues.LITTLE_ENDIAN);
- WKBWriter be = new WKBWriter(outDims, ByteOrderValues.BIG_ENDIAN);
- wkbLE = le.write(geo);
- wkbBE = be.write(geo);
+ boolean isXYZ = "XYZ".equals(dim);
+ wkbReadSingleLE = buildLineWKB(true, isXYZ, nVerticesPerLine);
+ wkbReadSingleBE = buildLineWKB(false, isXYZ, nVerticesPerLine);
+ wkbReadMultiLE = buildMultiLineWKB(true, isXYZ, nLines, nVerticesPerLine);
+ wkbReadMultiBE = buildMultiLineWKB(false, isXYZ, nLines, nVerticesPerLine);
}
- /** WKT → Geography (parse only) */
+ // ---- WRITE (Geography -> WKB) ----
@Benchmark
- public void wkt_read(Blackhole bh) throws ParseException, org.locationtech.jts.io.ParseException {
- WKTReader reader = new WKTReader();
- Geography g = reader.read(wkt);
- bh.consume(g);
- bh.consume(g.numShapes());
+ public double wkb_write_line(Blackhole bh, BenchPolylineWKB.ProfilerHook ph) throws IOException {
+ return write(geoLine, bh);
}
- /** Geography → WKB (serialize only) */
@Benchmark
- public double wkb_write(Blackhole bh, ProfilerHook ph) throws IOException {
- // choose output dimensions (2 = XY, 3 = XYZ)
+ public double wkb_write_multiline(Blackhole bh, BenchPolylineWKB.ProfilerHook ph)
+ throws IOException {
+ return write(geoMulti, bh);
+ }
+
+ private double write(Geography g, Blackhole bh) throws IOException {
int outDims = ("XY".equals(dim) ? 2 : 3);
int order =
"LE".equals(endianness) ? ByteOrderValues.LITTLE_ENDIAN : ByteOrderValues.BIG_ENDIAN;
-
- // do the actual write
WKBWriter writer = new WKBWriter(outDims, order);
- byte[] out = writer.write(geo);
+ byte[] out = writer.write(g);
long sum = 0;
- for (byte b : out) {
- sum += (b & 0xFF);
- }
+ for (byte b : out) sum += (b & 0xFF);
bh.consume(out);
return (double) sum;
}
- /** WKB → Geography (deserialize only) */
+ // ---- READ (WKB -> Geography) ----
@Benchmark
- public void wkb_read(Blackhole bh, ProfilerHook ph)
- throws IOException, org.locationtech.jts.io.ParseException {
+ public void wkb_read_line(Blackhole bh, BenchPolylineWKB.ProfilerHook ph)
+ throws IOException, ParseException {
+ read(("LE".equals(endianness) ? wkbReadSingleLE : wkbReadSingleBE), bh);
+ }
+
+ @Benchmark
+ public void wkb_read_multiline(Blackhole bh, BenchPolylineWKB.ProfilerHook ph)
+ throws IOException, ParseException {
+ read(("LE".equals(endianness) ? wkbReadMultiLE : wkbReadMultiBE), bh);
+ }
+
+ private void read(byte[] src, Blackhole bh) throws IOException, ParseException {
WKBReader reader = new WKBReader();
- byte[] src = "LE".equals(endianness) ? wkbLE : wkbBE;
Geography g = reader.read(src);
bh.consume(g);
bh.consume(g.numShapes());
}
- // ---------- helpers ----------
+ // ---- Hand-built WKB for READ benches ----
+
+ private static byte[] buildLineWKB(boolean little, boolean xyz, int nVertices) {
+ int type = xyz ? 1002 : 2; // LineString type
+ int doubles = xyz ? 3 : 2;
+ int len = 1 + 4 + 4 + (8 * doubles * nVertices); // endian, type, count, coords
+
+ ByteBuffer bb =
+ ByteBuffer.allocate(len).order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ bb.put(little ? (byte) 1 : (byte) 0);
+ bb.putInt(type);
+ bb.putInt(nVertices);
+ for (int i = 0; i < nVertices; i++) {
+ double[] c = pointCoord(i, xyz);
+ bb.putDouble(c[0]);
+ bb.putDouble(c[1]);
+ if (xyz) bb.putDouble(c[2]);
+ }
+ return bb.array();
+ }
+
+ private static byte[] buildMultiLineWKB(boolean little, boolean xyz, int nLines, int nVertices) {
+ int multiType = xyz ? 1005 : 5; // MultiLineString type
+ int header = 1 + 4 + 4; // endian, type, num_lines
+ byte[] singleLine = buildLineWKB(little, xyz, nVertices);
+ int len = header + (nLines * singleLine.length);
+
+ ByteBuffer bb =
+ ByteBuffer.allocate(len).order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
+ bb.put(little ? (byte) 1 : (byte) 0);
+ bb.putInt(multiType);
+ bb.putInt(nLines);
+ for (int i = 0; i < nLines; i++) {
+ bb.put(singleLine);
+ }
+ return bb.array();
+ }
+
+ private static double[] pointCoord(int i, boolean xyz) {
+ double x = 100.0 + i * 0.001;
+ double y = 50.0 + i * 0.002;
+ if (!xyz) return new double[] {x, y};
+ double z = (i % 17) + 0.375;
+ return new double[] {x, y, z};
+ }
+
+ // ---- Helpers ----
+ private static String buildLineWKT(int nVertices, String dim) {
+ String dimToken = "XYZ".equals(dim) ? " Z" : "";
+ String coords =
+ IntStream.range(0, nVertices)
+ .mapToObj(i -> coord(i, dim))
+ .collect(Collectors.joining(", "));
+ return "LINESTRING" + dimToken + " (" + coords + ")";
+ }
private static String buildMultiLineWKT(int numLines, int numVerticesPerLine, String dim) {
String dimToken = "XYZ".equals(dim) ? " Z" : "";
@@ -133,68 +192,73 @@ private static String buildMultiLineWKT(int numLines, int numVerticesPerLine, St
IntStream.range(0, numLines)
.mapToObj(
i -> {
- String vertices =
+ String coords =
IntStream.range(0, numVerticesPerLine)
.mapToObj(j -> coord(i * numVerticesPerLine + j, dim))
.collect(Collectors.joining(", "));
- return "(" + vertices + ")";
+ return "(" + coords + ")";
})
.collect(Collectors.joining(", "));
return "MULTILINESTRING" + dimToken + " (" + lines + ")";
}
- // Emit small non-zero Z in XYZ to ensure real parsing work
+ // emit deterministic coords; add non-zero Z in XYZ
private static String coord(int i, String dim) {
- double x = i, y = i;
+ double x = 100.0 + i * 0.001;
+ double y = 50.0 + i * 0.002;
if ("XY".equals(dim)) {
return String.format(Locale.ROOT, "%.6f %.6f", x, y);
- } else { // XYZ
- double z = (i % 11) + 0.25;
+ } else {
+ double z = (i % 17) + 0.375;
return String.format(Locale.ROOT, "%.6f %.6f %.6f", x, y, z);
}
}
+
// =====================================================================
- // == Async-profiler hook (per-iteration, not inside the benchmark) ==
+ // == Async-profiler hook (runs inside the fork) ==
// =====================================================================
+ // -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
public static class ProfilerHook {
@Param({"cpu"})
- public String event; // cpu | alloc | wall | lock
+ public String event;
@Param({"jfr"})
- public String format; // jfr | flamegraph | collapsed
+ public String format;
@Param({"1ms"})
- public String interval; // e.g., 1ms (for CPU), ignored by alloc
+ public String interval;
private AsyncProfiler profiler;
private Path outDir;
@Setup(Level.Trial)
public void trial() throws Exception {
- profiler = AsyncProfilerLoader.load();
+ profiler = AsyncProfiler.getInstance();
outDir = Paths.get("profiles");
Files.createDirectories(outDir);
}
@Setup(Level.Iteration)
public void start(BenchmarkParams b, IterationParams it) throws Exception {
- if (it.getType() != IterationType.MEASUREMENT) return; // skip warmups
- // Make a readable, unique file per iteration
+ if (it.getType() != IterationType.MEASUREMENT) return;
String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
File out =
outDir
- .resolve(base + (format.equals("jfr") ? ".jfr" : ".html"))
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
.toAbsolutePath()
.toFile();
- if ("jfr".equals(format)) {
- profiler.execute(
- String.format("start,jfr,event=%s,interval=%s,file=%s", event, interval, out));
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
} else {
- // For non-JFR, start now; we'll set file/output on stop
- profiler.execute(String.format("start,event=%s,interval=%s", event, interval));
+ profiler.execute("start," + common);
System.setProperty("ap.out", out.getAbsolutePath());
System.setProperty("ap.format", format);
}
@@ -203,7 +267,7 @@ public void start(BenchmarkParams b, IterationParams it) throws Exception {
@TearDown(Level.Iteration)
public void stop(IterationParams it) throws Exception {
if (it.getType() != IterationType.MEASUREMENT) return;
- if ("jfr".equals(format)) {
+ if ("jfr".equalsIgnoreCase(format)) {
profiler.execute("stop");
} else {
String file = System.getProperty("ap.out");
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPoint.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPoint.java
index 9ad402bbcc5..6d003d23c93 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPoint.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPoint.java
@@ -33,7 +33,6 @@
import java.util.List;
import java.util.concurrent.TimeUnit;
import one.profiler.AsyncProfiler;
-import one.profiler.AsyncProfilerLoader;
import org.apache.sedona.common.S2Geography.EncodeOptions;
import org.apache.sedona.common.S2Geography.EncodeTag;
import org.apache.sedona.common.S2Geography.Geography;
@@ -54,13 +53,13 @@
public class DecodeBenchPoint {
// -------- Params --------
- @Param({"1", "16", "256", "4096", "65536"})
+ @Param({"1", "16", "256", "1024", "4096", "65536"})
public int points;
@Param({"XY", "XYZ"})
public String dimension;
- @Param({"COMPACT"})
+ @Param({"COMPACT", "FAST"})
public String pointEncoding;
// -------- Reused points --------
@@ -75,6 +74,9 @@ public class DecodeBenchPoint {
// -------- Raw coder payloads (using the SAME pts) --------
private PrimitiveArrays.Bytes rawCompactBytesAdapter;
private PrimitiveArrays.Cursor compactCur;
+ private PrimitiveArrays.Bytes rawFastBytesAdapter;
+ private PrimitiveArrays.Cursor fastCur;
+
// ---------------- Setup ----------------
@Setup(Level.Trial)
public void setup() throws Exception {
@@ -111,6 +113,8 @@ public void setup() throws Exception {
// Bytes adapters & cursors
rawCompactBytesAdapter = bytesOverArray(rawCompactBytes);
compactCur = rawCompactBytesAdapter.cursor();
+ rawFastBytesAdapter = bytesOverArray(rawFastBytes);
+ fastCur = rawFastBytesAdapter.cursor();
System.out.printf(
"points=%d enc=%s tagged[POINT]=%dB tagged[MULTIPOINT]=%dB rawCompact=%dB rawFast=%dB%n",
@@ -129,6 +133,7 @@ public void rewind() {
taggedMultiPointIn.rewind();
// Reset S2 cursor positions
compactCur.position = 0;
+ fastCur.position = 0;
}
// =====================================================================
@@ -136,20 +141,23 @@ public void rewind() {
// =====================================================================
@Benchmark
- public void tagged_point_full(ProfilerHook ph, Blackhole bh) throws IOException {
+ public void tagged_point_decode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ throws IOException {
Geography g = Geography.decodeTagged(taggedPointIn);
bh.consume(g);
}
@Benchmark
- public void tagged_multipoint_decode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public void tagged_multipoint_decode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ throws IOException {
// Note: profiling is handled per-iteration by ProfilerHook; keep the body clean.
Geography g = Geography.decodeTagged(taggedMultiPointIn);
bh.consume(g);
}
@Benchmark
- public double tagged_multipoint_encode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public double tagged_multipoint_encode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ throws IOException {
EncodeOptions opts = new EncodeOptions();
applyPointEncodingPreference(opts, pointEncoding);
Geography g = new PointGeography(pts);
@@ -163,10 +171,11 @@ public double tagged_multipoint_encode(ProfilerHook ph, Blackhole bh) throws IOE
}
@Benchmark
- public double tagged_point_encode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public double tagged_point_encode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ throws IOException {
EncodeOptions opts = new EncodeOptions();
applyPointEncodingPreference(opts, pointEncoding);
- Geography g = new PointGeography(pts.get(0));
+ Geography g = new SinglePointGeography(pts.get(0));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
g.encodeTagged(baos, opts);
byte[] data = baos.toByteArray();
@@ -190,7 +199,7 @@ public int tagged_multipoint_tagOnly() throws IOException {
// =====================================================================
@Benchmark
- public double raw_S2points_compact_decode(ProfilerHook ph) throws IOException {
+ public double raw_S2points_compact_decode(DecodeBenchPoint.ProfilerHook ph) throws IOException {
List out = S2Point.Shape.COMPACT_CODER.decode(rawCompactBytesAdapter, compactCur);
double acc = 0;
for (int i = 0; i < out.size(); i++) {
@@ -201,7 +210,19 @@ public double raw_S2points_compact_decode(ProfilerHook ph) throws IOException {
}
@Benchmark
- public double raw_S2points_compact_encode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public double raw_S2points_fast_decode(DecodeBenchPoint.ProfilerHook ph) throws IOException {
+ List out = S2Point.Shape.FAST_CODER.decode(rawFastBytesAdapter, fastCur);
+ double acc = 0;
+ for (int i = 0; i < out.size(); i++) {
+ S2Point p = out.get(i);
+ acc += p.getX() + p.getY() + p.getZ();
+ }
+ return acc; // returning prevents DCE
+ }
+
+ @Benchmark
+ public double raw_S2points_compact_encode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output out = new Output(baos);
S2Point.Shape.COMPACT_CODER.encode(S2Point.Shape.fromList(pts), out);
@@ -213,6 +234,20 @@ public double raw_S2points_compact_encode(ProfilerHook ph, Blackhole bh) throws
return (double) s;
}
+ @Benchmark
+ public double raw_S2points_fast_encode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Output out = new Output(baos);
+ S2Point.Shape.FAST_CODER.encode(S2Point.Shape.fromList(pts), out);
+ // Materialize once to make the work observable & defeat DCE
+ byte[] arr = out.toBytes();
+ long s = 0;
+ for (byte b : arr) s += (b & 0xFF);
+ bh.consume(arr);
+ return (double) s;
+ }
+
// =====================================================================
// == Helpers ==
// =====================================================================
@@ -242,12 +277,11 @@ private static List buildPoints(int n) {
}
private static void applyPointEncodingPreference(EncodeOptions opts, String enc) {
- // Adjust to your real EncodeOptions API; placeholder keeps parity with your earlier code.
- // If you have an explicit "use compact vs fast" switch, set it here.
if ("COMPACT".equals(enc)) {
- opts.setEnableLazyDecode(false);
opts.setCodingHint(EncodeOptions.CodingHint.COMPACT);
- } else {
+ opts.setEnableLazyDecode(false);
+ } else if ("FAST".equals(enc)) {
+ opts.setCodingHint(EncodeOptions.CodingHint.FAST);
opts.setEnableLazyDecode(true);
}
}
@@ -274,55 +308,56 @@ public byte get(long i) {
// == Async-profiler hook (per-iteration, not inside the benchmark) ==
// =====================================================================
+ // -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
public static class ProfilerHook {
@Param({"cpu"})
- public String event; // cpu | alloc | wall | lock
+ public String event;
@Param({"jfr"})
- public String format; // jfr | flamegraph | collapsed
+ public String format;
@Param({"1ms"})
- public String interval; // e.g., 1ms (for CPU), ignored by alloc
+ public String interval;
private AsyncProfiler profiler;
private Path outDir;
@Setup(Level.Trial)
public void trial() throws Exception {
- profiler = AsyncProfilerLoader.load();
+ profiler = AsyncProfiler.getInstance();
outDir = Paths.get("profiles");
Files.createDirectories(outDir);
}
@Setup(Level.Iteration)
public void start(BenchmarkParams b, IterationParams it) throws Exception {
- if (it.getType() != IterationType.MEASUREMENT) return; // skip warmups
- // Make a readable, unique file per iteration
+ if (it.getType() != IterationType.MEASUREMENT) return;
String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
File out =
outDir
- .resolve(base + (format.equals("jfr") ? ".jfr" : ".html"))
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
.toAbsolutePath()
.toFile();
- if ("jfr".equals(format)) {
- profiler.execute(
- String.format("start,jfr,event=%s,interval=%s,file=%s", event, interval, out));
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
} else {
- // For non-JFR, start now; we'll set file/output on stop
- profiler.execute(String.format("start,event=%s,interval=%s", event, interval));
+ profiler.execute("start," + common);
System.setProperty("ap.out", out.getAbsolutePath());
System.setProperty("ap.format", format);
}
- // Optional sanity: System.out.println(profiler.execute("status"));
}
@TearDown(Level.Iteration)
public void stop(IterationParams it) throws Exception {
if (it.getType() != IterationType.MEASUREMENT) return;
- if ("jfr".equals(format)) {
+ if ("jfr".equalsIgnoreCase(format)) {
profiler.execute("stop");
} else {
String file = System.getProperty("ap.out");
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java
index fb0cd7b68d4..11aaa15abfb 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java
@@ -31,7 +31,6 @@
import java.util.List;
import java.util.concurrent.TimeUnit;
import one.profiler.AsyncProfiler;
-import one.profiler.AsyncProfilerLoader;
import org.apache.sedona.common.S2Geography.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.BenchmarkParams;
@@ -48,10 +47,10 @@
public class DecodeBenchPolygon {
// -------- Params --------
- @Param({"1", "1", "1", "1", "16", "256", "1028"})
+ @Param({"1", "16", "256", "1024"})
public int numPolygons;
- @Param({"4", "16", "256", "1028", "1028", "1028", "1028"})
+ @Param({"4", "16", "256", "1024"})
public int verticesPerPolygon;
@Param({"XY", "XYZ"})
@@ -131,19 +130,21 @@ public void rewind() {
// =====================================================================
@Benchmark
- public void tagged_polygon_full(ProfilerHook ph, Blackhole bh) throws IOException {
+ public void tagged_polygon_full(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
+ throws IOException {
Geography g = Geography.decodeTagged(taggedPolygonIn);
bh.consume(g);
}
@Benchmark
- public void tagged_multipolygon_full(ProfilerHook ph, Blackhole bh) throws IOException {
+ public void tagged_multipolygon_full(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
+ throws IOException {
Geography g = Geography.decodeTagged(taggedMultiPolygonIn);
bh.consume(g);
}
@Benchmark
- public double tagged_polygon_encode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
+ public double tagged_polygon_encode(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
throws IOException {
EncodeOptions opts = new EncodeOptions();
applyPointEncodingPreference(opts, pointEncoding);
@@ -158,7 +159,7 @@ public double tagged_polygon_encode(DecodeBenchPolyline.ProfilerHook ph, Blackho
}
@Benchmark
- public double tagged_multipolygon_encode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
+ public double tagged_multipolygon_encode(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
throws IOException {
EncodeOptions opts = new EncodeOptions();
applyPointEncodingPreference(opts, pointEncoding);
@@ -186,7 +187,7 @@ public int tagged_multipolygon_tagOnly() throws IOException {
// =====================================================================
@Benchmark
- public double raw_S2multipolygon_decode(ProfilerHook ph) throws IOException {
+ public double raw_S2multipolygon_decode(DecodeBenchPolygon.ProfilerHook ph) throws IOException {
int b0 = rawMultiPolygonIn.read();
int b1 = rawMultiPolygonIn.read();
int b2 = rawMultiPolygonIn.read();
@@ -207,7 +208,7 @@ public double raw_S2multipolygon_decode(ProfilerHook ph) throws IOException {
}
@Benchmark
- public double raw_S2polygon_compact_encode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ public double raw_S2polygon_compact_encode(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output out = new Output(baos);
@@ -296,44 +297,47 @@ private static void applyPointEncodingPreference(EncodeOptions opts, String enc)
// == Async-profiler hook (per-iteration, not inside the benchmark) ==
// =====================================================================
+ // -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
public static class ProfilerHook {
@Param({"cpu"})
- public String event; // cpu | alloc | wall | lock
+ public String event;
@Param({"jfr"})
- public String format; // jfr | flamegraph | collapsed
+ public String format;
@Param({"1ms"})
- public String interval; // e.g., 1ms (for CPU), ignored by alloc
+ public String interval;
private AsyncProfiler profiler;
private Path outDir;
@Setup(Level.Trial)
public void trial() throws Exception {
- profiler = AsyncProfilerLoader.load();
+ profiler = AsyncProfiler.getInstance();
outDir = Paths.get("profiles");
Files.createDirectories(outDir);
}
@Setup(Level.Iteration)
public void start(BenchmarkParams b, IterationParams it) throws Exception {
- if (it.getType() != IterationType.MEASUREMENT) return; // skip warmups
- // Make a readable, unique file per iteration
+ if (it.getType() != IterationType.MEASUREMENT) return;
String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
File out =
outDir
- .resolve(base + (format.equals("jfr") ? ".jfr" : ".html"))
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
.toAbsolutePath()
.toFile();
- if ("jfr".equals(format)) {
- profiler.execute(
- String.format("start,jfr,event=%s,interval=%s,file=%s", event, interval, out));
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
} else {
- // For non-JFR, start now; we'll set file/output on stop
- profiler.execute(String.format("start,event=%s,interval=%s", event, interval));
+ profiler.execute("start," + common);
System.setProperty("ap.out", out.getAbsolutePath());
System.setProperty("ap.format", format);
}
@@ -342,7 +346,7 @@ public void start(BenchmarkParams b, IterationParams it) throws Exception {
@TearDown(Level.Iteration)
public void stop(IterationParams it) throws Exception {
if (it.getType() != IterationType.MEASUREMENT) return;
- if ("jfr".equals(format)) {
+ if ("jfr".equalsIgnoreCase(format)) {
profiler.execute("stop");
} else {
String file = System.getProperty("ap.out");
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolyline.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolyline.java
index aa77c589d00..9ae1ef70d6f 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolyline.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolyline.java
@@ -31,7 +31,6 @@
import java.util.List;
import java.util.concurrent.TimeUnit;
import one.profiler.AsyncProfiler;
-import one.profiler.AsyncProfilerLoader;
import org.apache.sedona.common.S2Geography.*;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.BenchmarkParams;
@@ -48,10 +47,10 @@
public class DecodeBenchPolyline {
// -------- Params --------
- @Param({"1", "1", "1", "1", "16", "256", "1028"})
+ @Param({"1", "16", "256", "1024"})
public int numPolylines;
- @Param({"2", "16", "256", "1028", "1028", "1028", "1028"})
+ @Param({"2", "16", "256", "1024"})
public int verticesPerPolyline;
@Param({"XY", "XYZ"})
@@ -131,20 +130,23 @@ public void rewind() {
// =====================================================================
@Benchmark
- public void tagged_polyline_decode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public void tagged_polyline_decode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
+ throws IOException {
Geography g = Geography.decodeTagged(taggedPolylineIn);
bh.consume(g);
}
@Benchmark
- public void tagged_multipolyline_decode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public void tagged_multipolyline_decode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
+ throws IOException {
// Note: profiling is handled per-iteration by ProfilerHook; keep the body clean.
Geography g = Geography.decodeTagged(taggedMultiPolylineIn);
bh.consume(g);
}
@Benchmark
- public double tagged_polyline_encode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public double tagged_polyline_encode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
+ throws IOException {
EncodeOptions opts = new EncodeOptions();
applyPointEncodingPreference(opts, pointEncoding);
Geography g = new PolylineGeography(polylines.get(0));
@@ -158,7 +160,8 @@ public double tagged_polyline_encode(ProfilerHook ph, Blackhole bh) throws IOExc
}
@Benchmark
- public double tagged_multipolyline_encode(ProfilerHook ph, Blackhole bh) throws IOException {
+ public double tagged_multipolyline_encode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
+ throws IOException {
EncodeOptions opts = new EncodeOptions();
applyPointEncodingPreference(opts, pointEncoding);
Geography g = new PolylineGeography(polylines);
@@ -185,7 +188,7 @@ public int tagged_multipolyline_tagOnly() throws IOException {
// =====================================================================
@Benchmark
- public double raw_S2multipolyline_decode(ProfilerHook ph) throws IOException {
+ public double raw_S2multipolyline_decode(DecodeBenchPolyline.ProfilerHook ph) throws IOException {
int b0 = rawMultiPolylineIn.read();
int b1 = rawMultiPolylineIn.read();
int b2 = rawMultiPolylineIn.read();
@@ -209,7 +212,7 @@ public double raw_S2multipolyline_decode(ProfilerHook ph) throws IOException {
}
@Benchmark
- public double raw_S2polyline_compact_encode(DecodeBenchPoint.ProfilerHook ph, Blackhole bh)
+ public double raw_S2polyline_compact_encode(DecodeBenchPolyline.ProfilerHook ph, Blackhole bh)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output out = new Output(baos);
@@ -292,44 +295,47 @@ private static void applyPointEncodingPreference(EncodeOptions opts, String enc)
// == Async-profiler hook (per-iteration, not inside the benchmark) ==
// =====================================================================
+ // -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
public static class ProfilerHook {
@Param({"cpu"})
- public String event; // cpu | alloc | wall | lock
+ public String event;
@Param({"jfr"})
- public String format; // jfr | flamegraph | collapsed
+ public String format;
@Param({"1ms"})
- public String interval; // e.g., 1ms (for CPU), ignored by alloc
+ public String interval;
private AsyncProfiler profiler;
private Path outDir;
@Setup(Level.Trial)
public void trial() throws Exception {
- profiler = AsyncProfilerLoader.load();
+ profiler = AsyncProfiler.getInstance();
outDir = Paths.get("profiles");
Files.createDirectories(outDir);
}
@Setup(Level.Iteration)
public void start(BenchmarkParams b, IterationParams it) throws Exception {
- if (it.getType() != IterationType.MEASUREMENT) return; // skip warmups
- // Make a readable, unique file per iteration
+ if (it.getType() != IterationType.MEASUREMENT) return;
String base = String.format("%s-iter%02d-%s", b.getBenchmark(), it.getCount(), event);
File out =
outDir
- .resolve(base + (format.equals("jfr") ? ".jfr" : ".html"))
+ .resolve(base + (format.equalsIgnoreCase("jfr") ? ".jfr" : ".html"))
.toAbsolutePath()
.toFile();
- if ("jfr".equals(format)) {
- profiler.execute(
- String.format("start,jfr,event=%s,interval=%s,file=%s", event, interval, out));
+
+ // Using 'all-user' helps the profiler find the correct forked JMH process.
+ // The filter is removed to avoid accidentally hiding the benchmark thread.
+ String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
+
+ if ("jfr".equalsIgnoreCase(format)) {
+ profiler.execute("start," + common + ",jfr,file=" + out.getAbsolutePath());
} else {
- // For non-JFR, start now; we'll set file/output on stop
- profiler.execute(String.format("start,event=%s,interval=%s", event, interval));
+ profiler.execute("start," + common);
System.setProperty("ap.out", out.getAbsolutePath());
System.setProperty("ap.format", format);
}
@@ -338,7 +344,7 @@ public void start(BenchmarkParams b, IterationParams it) throws Exception {
@TearDown(Level.Iteration)
public void stop(IterationParams it) throws Exception {
if (it.getType() != IterationType.MEASUREMENT) return;
- if ("jfr".equals(format)) {
+ if ("jfr".equalsIgnoreCase(format)) {
profiler.execute("stop");
} else {
String file = System.getProperty("ap.out");
From b65484839f397783b1a110dd09b1c717772bd006 Mon Sep 17 00:00:00 2001
From: Zhuocheng Shang <122398181+ZhuochengShang@users.noreply.github.com>
Date: Thu, 28 Aug 2025 13:15:39 -0700
Subject: [PATCH 8/8] benchmark updates
---
.../apache/sedona/bench/BenchPointWKB.java | 57 +++++++++++--------
.../apache/sedona/bench/BenchPolygonWKB.java | 4 +-
.../sedona/bench/DecodeBenchPolygon.java | 4 +-
3 files changed, 37 insertions(+), 28 deletions(-)
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java
index e6e57ded144..62e6eea21f1 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPointWKB.java
@@ -35,7 +35,9 @@
import org.locationtech.jts.io.ByteOrderValues;
import org.locationtech.jts.io.ParseException;
import org.openjdk.jmh.annotations.*;
-import org.openjdk.jmh.infra.*;
+import org.openjdk.jmh.infra.BenchmarkParams;
+import org.openjdk.jmh.infra.Blackhole;
+import org.openjdk.jmh.infra.IterationParams;
import org.openjdk.jmh.runner.IterationType;
@BenchmarkMode(Mode.AverageTime)
@@ -59,46 +61,36 @@ public class BenchPointWKB {
private String wktPoint, wktMulti;
private Geography geoPoint, geoMulti;
- // use writer for write benches (to benchmark it)
- private byte[] wkbPointLE, wkbPointBE;
-
- // use hand-built payloads for read benches (so reader always accepts them)
+ // Hand-built payloads for READ benches (explicit WKB layout)
private byte[] wkbReadPointLE, wkbReadPointBE;
private byte[] wkbReadMultiLE, wkbReadMultiBE;
@Setup(Level.Trial)
public void setup() throws ParseException, IOException {
wktPoint = buildPointWKT(dim);
- wktMulti = buildMultiPointWKT(nPoints, dim);
+ wktMulti = buildMultiPointWKT(nPoints, dim); // <-- double-paren per-point
WKTReader wktReader = new WKTReader();
geoPoint = wktReader.read(wktPoint);
geoMulti = wktReader.read(wktMulti);
- // Precompute writer outputs (so write benches time only serialization)
- int outDims = "XY".equals(dim) ? 2 : 3;
- WKBWriter le = new WKBWriter(outDims, ByteOrderValues.LITTLE_ENDIAN);
- WKBWriter be = new WKBWriter(outDims, ByteOrderValues.BIG_ENDIAN);
- wkbPointLE = le.write(geoPoint);
- wkbPointBE = be.write(geoPoint);
-
// Precompute READ payloads with explicit layout the reader expects
boolean isXYZ = "XYZ".equals(dim);
- wkbReadPointLE = buildPointWKB(/*little=*/ true, isXYZ, 0);
- wkbReadPointBE = buildPointWKB(/*little=*/ false, isXYZ, 0);
- wkbReadMultiLE = buildMultiPointWKB(/*little=*/ true, isXYZ, nPoints);
- wkbReadMultiBE = buildMultiPointWKB(/*little=*/ false, isXYZ, nPoints);
+ wkbReadPointLE = buildPointWKB(true, isXYZ, 0);
+ wkbReadPointBE = buildPointWKB(false, isXYZ, 0);
+ wkbReadMultiLE = buildMultiPointWKB(true, isXYZ, nPoints);
+ wkbReadMultiBE = buildMultiPointWKB(false, isXYZ, nPoints);
}
// ---------------- WRITE (Geography -> WKB) ----------------
@Benchmark
- public double wkb_write_point(Blackhole bh, BenchPolygonWKB.ProfilerHook ph) throws IOException {
+ public double wkb_write_point(Blackhole bh, BenchPointWKB.ProfilerHook ph) throws IOException {
return write(geoPoint, bh);
}
@Benchmark
- public double wkb_write_multipoint(Blackhole bh, BenchPolygonWKB.ProfilerHook ph)
+ public double wkb_write_multipoint(Blackhole bh, BenchPointWKB.ProfilerHook ph)
throws IOException {
return write(geoMulti, bh);
}
@@ -171,12 +163,12 @@ private static byte[] buildMultiPointWKB(boolean little, boolean xyz, int n) {
ByteBuffer.allocate(header + n * perPoint)
.order(little ? ByteOrder.LITTLE_ENDIAN : ByteOrder.BIG_ENDIAN);
- // Write MultiPoint header
+ // MultiPoint header
bb.put(little ? (byte) 1 : (byte) 0);
bb.putInt(multiType);
bb.putInt(n);
- // THE FIX: Write each Point as a full WKB geometry, as the reader expects
+ // Each Point as a full WKB geometry
for (int i = 0; i < n; i++) {
bb.put(little ? (byte) 1 : (byte) 0); // inner endian
bb.putInt(pointType); // inner type (POINT)
@@ -202,11 +194,14 @@ private static String buildPointWKT(String dim) {
return "POINT" + ("XYZ".equals(dim) ? " Z" : "") + " (" + coord(0, dim) + ")";
}
+ // IMPORTANT: MULTIPOINT requires double-paren ((x y), (x y), ...)
private static String buildMultiPointWKT(int n, String dim) {
if (n <= 1) return buildPointWKT(dim);
String dimToken = "XYZ".equals(dim) ? " Z" : "";
String pts =
- IntStream.range(0, n).mapToObj(i -> coord(i, dim)).collect(Collectors.joining(", "));
+ IntStream.range(0, n)
+ .mapToObj(i -> "(" + coord(i, dim) + ")") // wrap each point!
+ .collect(Collectors.joining(", "));
return "MULTIPOINT" + dimToken + " (" + pts + ")";
}
@@ -218,6 +213,22 @@ private static String coord(int i, String dim) {
return String.format(Locale.ROOT, "%.6f %.6f %.6f", x, y, z);
}
+ // -------- Sanity: confirm shape count/bytes scale with nPoints --------
+ @TearDown(Level.Trial)
+ public void sanity() throws IOException {
+ int outDims = "XY".equals(dim) ? 2 : 3;
+ int order =
+ "LE".equals(endianness) ? ByteOrderValues.LITTLE_ENDIAN : ByteOrderValues.BIG_ENDIAN;
+ WKBWriter w = new WKBWriter(outDims, order);
+ byte[] out = w.write(geoMulti);
+
+ int coordsPerPt = "XY".equals(dim) ? 2 : 3;
+ int expectedApprox = 1 + 4 + 4 + nPoints * (1 + 4 + 8 * coordsPerPt); // header + N*(point)
+ System.out.printf(
+ "sanity: dim=%s order=%s n=%d shapes=%d bytes=%d (~%d)%n",
+ dim, endianness, nPoints, geoMulti.numShapes(), out.length, expectedApprox);
+ }
+
// -------- Async-profiler hook (runs inside fork) --------
/** Per-iteration profiler: start on measurement iterations, stop after each iteration. */
@State(Scope.Benchmark)
@@ -251,8 +262,6 @@ public void start(BenchmarkParams b, IterationParams it) throws Exception {
.toAbsolutePath()
.toFile();
- // Using 'all-user' helps the profiler find the correct forked JMH process.
- // The filter is removed to avoid accidentally hiding the benchmark thread.
String common = String.format("event=%s,interval=%s,cstack=fp,threads", event, interval);
if ("jfr".equalsIgnoreCase(format)) {
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java
index 1ebbecbc4c0..6d332b8358d 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/BenchPolygonWKB.java
@@ -50,11 +50,11 @@ public class BenchPolygonWKB {
public String dim;
/** number of polygons in MULTIPOLYGON */
- @Param({"1", "16", "256"})
+ @Param({"1", "16", "256", "1024"})
public int nPolygons;
/** vertices per polygon outer ring (>=4; last coord auto‑closed) */
- @Param({"4", "16", "256"})
+ @Param({"4", "16", "256", "1024"})
public int nVerticesPerRing;
/** WKB endianness */
diff --git a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java
index 11aaa15abfb..ddad731680e 100644
--- a/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java
+++ b/geographyBench/src/jmh/java/org/apache/sedona/bench/DecodeBenchPolygon.java
@@ -137,7 +137,7 @@ public void tagged_polygon_full(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh
}
@Benchmark
- public void tagged_multipolygon_full(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
+ public void tagged_multipolygon_decode(DecodeBenchPolygon.ProfilerHook ph, Blackhole bh)
throws IOException {
Geography g = Geography.decodeTagged(taggedMultiPolygonIn);
bh.consume(g);
@@ -202,7 +202,7 @@ public double raw_S2multipolygon_decode(DecodeBenchPolygon.ProfilerHook ph) thro
double acc = 0;
// Consume the data to prevent Dead Code Elimination (DCE)
for (S2Polygon polygon : out) {
- acc += polygon.getArea();
+ acc += polygon.numLoops();
}
return acc;
}