diff --git a/pom.xml b/pom.xml index 09d312d..c82eed2 100644 --- a/pom.xml +++ b/pom.xml @@ -113,8 +113,10 @@ 7.1.0 3.6.0 2.9.1 + 3.5.0 3.4.1 3.14.0 + 3.8.1 3.1.4 3.5.0 3.2.7 @@ -136,6 +138,7 @@ 4.9.3 3.1.1 + 1.37 20250107 5.12.1 1.4.0 @@ -175,6 +178,12 @@ provided true + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + org.json json @@ -265,10 +274,10 @@ + ~ Parses the version into components. + ~ + ~ The parsed version is used to generate the `Specification-Version` manifest header. + --> org.codehaus.mojo build-helper-maven-plugin @@ -318,16 +327,16 @@ --should-stop=ifError=FLOW -Xplugin:ErrorProne + ~ Due to a bug in IntelliJ IDEA, annotation processing MUST be enabled. + ~ Failing to do so will cause IDEA to ignore the annotation processor path + ~ and choke on the Error Prone compiler arguments. + ~ + ~ On the other hand, we cannot pass an empty `annotationProcessors` list to Maven, + ~ since the `-processor` compiler argument requires at least one processor class name. + ~ + ~ If you add an annotation processor, please also add an `annotationProcessors` configuration + ~ option. + --> @@ -337,6 +346,26 @@ + + + default-testCompile + + + -proc:full + + + org.openjdk.jmh.generators.BenchmarkProcessor + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + com.diffplug.spotless @@ -602,5 +631,54 @@ + + + benchmark + + .* + true + + + test-compile + dependency:build-classpath@build-classpath + exec:exec@run-benchmark + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.plugin.version} + + + build-classpath + + build-classpath + + + test + test.classpath + + + + + + org.codehaus.mojo + exec-maven-plugin + ${exec.maven.plugin.version} + + + run-benchmark + + exec + + + ${java.home}/bin/java + -cp target/classes:target/test-classes:${test.classpath} org.openjdk.jmh.Main ${jmh.args} + + + + + + + diff --git a/src/main/java/com/github/packageurl/PackageURL.java b/src/main/java/com/github/packageurl/PackageURL.java index 18db0f9..04749ee 100644 --- a/src/main/java/com/github/packageurl/PackageURL.java +++ b/src/main/java/com/github/packageurl/PackageURL.java @@ -610,6 +610,7 @@ private static byte percentDecode(final byte[] bytes, final int start) { return ((byte) ((c1 << 4) + c2)); } + // package-private for testing static String percentDecode(final String source) { if (source.isEmpty()) { return source; @@ -659,6 +660,7 @@ private static boolean isPercent(int c) { return (c == PERCENT_CHAR); } + // package-private for testing static String percentEncode(final String source) { if (source.isEmpty()) { return source; diff --git a/src/test/java/com/github/packageurl/PercentEncodingBenchmark.java b/src/test/java/com/github/packageurl/PercentEncodingBenchmark.java new file mode 100644 index 0000000..17b421c --- /dev/null +++ b/src/test/java/com/github/packageurl/PercentEncodingBenchmark.java @@ -0,0 +1,127 @@ +/* + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.github.packageurl; + +import java.nio.charset.StandardCharsets; +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.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.infra.Blackhole; + +/** + * Measures the performance of performance decoding and encoding. + *

+ * Run the benchmark with: + *

+ *
+ *     mvn -Pbenchmark
+ * 
+ *

+ * To pass arguments to JMH use: + *

+ *
+ *     mvn -Pbenchmark -Djmh.args=""
+ * 
+ */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +public class PercentEncodingBenchmark { + + private static final int DATA_COUNT = 1000; + private static final int DECODED_LENGTH = 256; + private static final byte[] UNRESERVED = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~".getBytes(StandardCharsets.US_ASCII); + + @Param({"0", "0.1", "0.5"}) + private double nonAsciiProb; + + private String[] decodedData = createDecodedData(); + private String[] encodedData = encodeData(decodedData); + + @Setup + public void setup() { + decodedData = createDecodedData(); + encodedData = encodeData(encodedData); + } + + private String[] createDecodedData() { + Random random = new Random(); + String[] decodedData = new String[DATA_COUNT]; + for (int i = 0; i < DATA_COUNT; i++) { + char[] chars = new char[DECODED_LENGTH]; + for (int j = 0; j < DECODED_LENGTH; j++) { + if (random.nextDouble() < nonAsciiProb) { + chars[j] = (char) (Byte.MAX_VALUE + 1 + random.nextInt(Short.MAX_VALUE - Byte.MAX_VALUE - 1)); + } else { + chars[j] = (char) UNRESERVED[random.nextInt(UNRESERVED.length)]; + } + } + decodedData[i] = new String(chars); + } + return decodedData; + } + + private static String[] encodeData(String[] decodedData) { + String[] encodedData = new String[decodedData.length]; + for (int i = 0; i < decodedData.length; i++) { + encodedData[i] = PackageURL.percentEncode(decodedData[i]); + } + return encodedData; + } + + @Benchmark + public void baseline(Blackhole blackhole) { + for (int i = 0; i < DATA_COUNT; i++) { + byte[] buffer = decodedData[i].getBytes(StandardCharsets.UTF_8); + // Change the String a little bit + for (int idx = 0; idx < buffer.length; idx++) { + byte b = buffer[idx]; + if ('a' <= b && b <= 'z') { + buffer[idx] = (byte) (b & 0x20); + } + } + blackhole.consume(new String(buffer, StandardCharsets.UTF_8)); + } + } + + @Benchmark + public void percentDecode(final Blackhole blackhole) { + for (int i = 0; i < DATA_COUNT; i++) { + blackhole.consume(PackageURL.percentDecode(encodedData[i])); + } + } + + @Benchmark + public void percentEncode(final Blackhole blackhole) { + for (int i = 0; i < DATA_COUNT; i++) { + blackhole.consume(PackageURL.percentEncode(decodedData[i])); + } + } +}