From a00732a80c76dbc3046390107169122f8b70a4f7 Mon Sep 17 00:00:00 2001 From: Tobias Hotz Date: Fri, 6 Sep 2024 13:34:05 +0200 Subject: [PATCH 1/3] Add initial JMH infrastructure and benchmarks JMH allows per-method benchmarks. See jmh/README for more details --- jmh/.gitignore | 1 + jmh/README.md | 22 +++++ jmh/pom.xml | 93 +++++++++++++++++++ .../fao/geonet/domain/ISODateBenchmark.java | 23 +++++ pom.xml | 6 ++ 5 files changed, 145 insertions(+) create mode 100644 jmh/.gitignore create mode 100644 jmh/README.md create mode 100644 jmh/pom.xml create mode 100644 jmh/src/main/java/org/fao/geonet/domain/ISODateBenchmark.java diff --git a/jmh/.gitignore b/jmh/.gitignore new file mode 100644 index 00000000000..916e17c097a --- /dev/null +++ b/jmh/.gitignore @@ -0,0 +1 @@ +dependency-reduced-pom.xml diff --git a/jmh/README.md b/jmh/README.md new file mode 100644 index 00000000000..c70b67f807a --- /dev/null +++ b/jmh/README.md @@ -0,0 +1,22 @@ +# Geonetwork JMH Benchmark Suite + +This module contains micro benchmarks for some GN functions. +It is used to validate the performance of individual functions and snippets of code. +For performance tests at a larger scale, consider using another tool like jmeter + +To get started using JMH, see the [JMH docs](https://github.com/openjdk/jmh) + +## Adding new benchmarks + +New benchmark can be added by simply +1. Adding the module of the class to test to the gn-jmh module +2. Writing a benchmark in this module + +## Running the benchmarks + +1. Make sure the `jmh` profile is enabled (or enable it using `-Pjmh` when running maven) +2. Run `mvn verify` to build the benchmarks +3. Run the benchmark using `java -jar jmh/target/benchmarks.jar` in this module + +If you want to get additional inside, you can append `--prof stack`, which outputs text-base stack sampling, or `--prof jfr` to get java flight recorder profile that can be read by applications like VisualVM. +Make sure to only use additional profilers when analysing the data, not when actually recording numbers. diff --git a/jmh/pom.xml b/jmh/pom.xml new file mode 100644 index 00000000000..d8b2c53a62c --- /dev/null +++ b/jmh/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + geonetwork + org.geonetwork-opensource + 4.4.6-SNAPSHOT + + + gn-jmh + jar + JMH benchmarks for Geonetwork + + + + ${project.groupId} + gn-domain + ${project.version} + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + UTF-8 + + 1.37 + + benchmarks + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + + -processor + org.openjdk.jmh.generators.BenchmarkProcessor + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + ${uberjar.name} + + + org.openjdk.jmh.Main + + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + diff --git a/jmh/src/main/java/org/fao/geonet/domain/ISODateBenchmark.java b/jmh/src/main/java/org/fao/geonet/domain/ISODateBenchmark.java new file mode 100644 index 00000000000..8d156fd1c48 --- /dev/null +++ b/jmh/src/main/java/org/fao/geonet/domain/ISODateBenchmark.java @@ -0,0 +1,23 @@ +package org.fao.geonet.domain; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(1) +public class ISODateBenchmark { + @Param({"1976-06-03", "1976/06/03", "24-06-06"}) + public String arg; + + @Benchmark + public void measureIsoSimple(Blackhole bh) { + ISODate isoDate = new ISODate(arg); + bh.consume(isoDate); + } +} diff --git a/pom.xml b/pom.xml index 4c491177d3f..8d45551fd21 100644 --- a/pom.xml +++ b/pom.xml @@ -1437,6 +1437,12 @@ jmeter + + jmh + + jmh + + macOS From 8d5e5e64305d055147d972223aef0500e5fce70f Mon Sep 17 00:00:00 2001 From: Tobias Hotz Date: Tue, 24 Sep 2024 12:34:57 +0200 Subject: [PATCH 2/3] Improve the performance of simple dates, take 1 Simples dates are common in many cases. Performance analysis showed that this can be a performance hotspot during harvesting. This is due to the design of UUIDMapper, which loads all metadata for a harvester for every new batch. This can not be easily changed, but we can improve the performance here, which also helps other code paths using this method. This change improves the performance of the method by a factor of about 1.3x --- domain/src/main/java/org/fao/geonet/domain/ISODate.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/domain/src/main/java/org/fao/geonet/domain/ISODate.java b/domain/src/main/java/org/fao/geonet/domain/ISODate.java index c2166fd2540..6328f7345c5 100644 --- a/domain/src/main/java/org/fao/geonet/domain/ISODate.java +++ b/domain/src/main/java/org/fao/geonet/domain/ISODate.java @@ -40,6 +40,7 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Date; +import java.util.regex.Pattern; /** * Represents a date at a given time. Provides methods for representing the date as a string and @@ -52,6 +53,7 @@ public class ISODate implements Cloneable, Comparable, Serializable, Xm @XmlTransient public static final DateTimeFormatter YEAR_MONTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); @XmlTransient public static final DateTimeFormatter YEAR_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); @XmlTransient public static final DateTimeFormatter YEAR_MONTH_DAYS_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + @XmlTransient private static final Pattern SIMPLE_SPLIT_PATTERN = Pattern.compile("[-/]"); /** * {@code true} if the format is {@code yyyy-mm-dd}. @@ -370,7 +372,7 @@ public boolean equals(Object obj) { private void parseDate(@Nonnull String isoDate) { try { - String[] parts = isoDate.split("[-/]"); + String[] parts = SIMPLE_SPLIT_PATTERN.split(isoDate); if ((parts.length == 0) || (parts.length > 3)) { throw new IllegalArgumentException("Invalid ISO date: " + isoDate); } From 3c90f55e8c82e5b97ebcfa537d65a319f25f89e5 Mon Sep 17 00:00:00 2001 From: Tobias Hotz Date: Tue, 24 Sep 2024 12:41:51 +0200 Subject: [PATCH 3/3] Improve the performance of simple dates, take 2 This change improves the performance of parseDate by removing as many string operations as possible. Doing this and some other minor optimisations, we can improve the performance so it is about 5x faster than the original. --- .../java/org/fao/geonet/domain/ISODate.java | 107 +++++++++++------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/domain/src/main/java/org/fao/geonet/domain/ISODate.java b/domain/src/main/java/org/fao/geonet/domain/ISODate.java index 6328f7345c5..7aada4b4863 100644 --- a/domain/src/main/java/org/fao/geonet/domain/ISODate.java +++ b/domain/src/main/java/org/fao/geonet/domain/ISODate.java @@ -40,7 +40,6 @@ import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Date; -import java.util.regex.Pattern; /** * Represents a date at a given time. Provides methods for representing the date as a string and @@ -53,7 +52,6 @@ public class ISODate implements Cloneable, Comparable, Serializable, Xm @XmlTransient public static final DateTimeFormatter YEAR_MONTH_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM"); @XmlTransient public static final DateTimeFormatter YEAR_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); @XmlTransient public static final DateTimeFormatter YEAR_MONTH_DAYS_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - @XmlTransient private static final Pattern SIMPLE_SPLIT_PATTERN = Pattern.compile("[-/]"); /** * {@code true} if the format is {@code yyyy-mm-dd}. @@ -211,6 +209,7 @@ public String getDateAndTime() { } } + public void setDateAndTime(String isoDate) { String timeAndDate = isoDate; @@ -372,58 +371,82 @@ public boolean equals(Object obj) { private void parseDate(@Nonnull String isoDate) { try { - String[] parts = SIMPLE_SPLIT_PATTERN.split(isoDate); - if ((parts.length == 0) || (parts.length > 3)) { - throw new IllegalArgumentException("Invalid ISO date: " + isoDate); - } + int startPos = 0; + int partNumber = 0; + int year = -1; + int month = -1; + int day = -1; - _shortDate = (parts.length == 3); - _shortDateYearMonth = (parts.length == 2); - _shortDateYear = (parts.length == 1); - - int year; - if (parts[0].length() < 4) { - int shortYear = Integer.parseInt(parts[0]); - String thisYear = String.valueOf(ZonedDateTime.now(ZoneOffset.UTC).getYear()); - int century = Integer.parseInt(thisYear.substring(0, 2)) * 100; - int yearInCentury = Integer.parseInt(thisYear.substring(2)); - - if (shortYear <= yearInCentury) { - year = century + shortYear; - } else { - year = century - 100 + shortYear; + ZoneId offset = ZoneId.systemDefault(); + // canonicalize the string + isoDate = isoDate.replace('/', '-'); + // until we've processed the whole string + while (startPos < isoDate.length()) { + if (partNumber >= 3) { + break; } - } else { - year = Integer.parseInt(parts[0]); + // try to find the next chunk + int nextPos = isoDate.indexOf('-', startPos); + if (nextPos == -1) { + // no next chunk to be found? This means this is the last chunk, process it accordingly + nextPos = isoDate.length(); + } + String subString = isoDate.substring(startPos, nextPos); + switch (partNumber) { + case 0: + // First part: year + int parsedInt = Integer.parseInt(subString); + if ((nextPos - startPos) < 4) { + int thisYear = ZonedDateTime.now(ZoneOffset.UTC).getYear(); + int century = thisYear / 100; + int yearInCentury = thisYear % 100; + year = (century * 100) + parsedInt; + if (parsedInt > yearInCentury) { + // If year is 2024, turn 32-05-05 into 1932, not 2032 + year -= 100; + } + } else { + year = parsedInt; + } + break; + case 1: + // Second part: month + month = Integer.parseInt(subString); + break; + case 2: + // Third part: day + if (subString.toLowerCase().endsWith("z")) { + offset = ZoneOffset.UTC; + day = Integer.parseInt(subString.substring(0, subString.length() - 1)); + } else { + day = Integer.parseInt(subString); + } + break; + default: + throw new IllegalStateException("Should not reach partNumber " + partNumber); + } + partNumber++; + startPos = nextPos + 1; } + if (partNumber == 0 || partNumber > 3) { + throw new IllegalArgumentException("Invalid ISO date: " + isoDate); + } + + _shortDate = (partNumber == 3); + _shortDateYearMonth = (partNumber == 2); + _shortDateYear = (partNumber == 1); - int month; - if (_shortDate || _shortDateYearMonth) { - month = Integer.parseInt(parts[1]); - } else { + if (!_shortDate && !_shortDateYearMonth) { month = 12; } - int day; - ZoneId offset = ZoneId.systemDefault(); - if (_shortDate) { - - if (parts[2].toLowerCase().endsWith("z")) { - offset = ZoneOffset.UTC; - day = Integer.parseInt(parts[2].substring(0, parts[2].length() - 1)); - } else { - day = Integer.parseInt(parts[2]); - } - } else { + if (!_shortDate) { // Calculate the last day for the year/month day = YearMonth.of(year, month).atEndOfMonth().getDayOfMonth(); } _shortDate = true; - internalDateTime = ZonedDateTime.now(offset).withYear(year).withMonth(month).withDayOfMonth(day).withHour(0).withMinute(0) - .withSecond(0).withNano(0); - //..ZonedDateTime.of(year, month, day, hour, minute, second, 0, offset); - + internalDateTime = ZonedDateTime.of(year, month, day, 0, 0, 0, 0, offset); } catch (Exception e) { throw new IllegalArgumentException("Invalid ISO date: " + isoDate, e); }