Skip to content

Commit bd7c019

Browse files
committed
feat: add a small benchmark for percent encoding decoding
This adds a small benchmark for percent encoding/decoding algorithms. You can run it with: ``` mvn -Pbenchmark ``` To pass arguments to JMH use `-Djmh.args="<arguments>"`.
1 parent a38ccd7 commit bd7c019

File tree

3 files changed

+217
-16
lines changed

3 files changed

+217
-16
lines changed

pom.xml

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,10 @@
113113
<bnd.maven.plugin.version>7.1.0</bnd.maven.plugin.version>
114114
<builder.helper.maven.plugin.version>3.6.0</builder.helper.maven.plugin.version>
115115
<cyclonedx-maven-plugin.version>2.9.1</cyclonedx-maven-plugin.version>
116+
<exec.maven.plugin.version>3.5.0</exec.maven.plugin.version>
116117
<maven.clean.plugin.version>3.4.1</maven.clean.plugin.version>
117118
<maven.compiler.plugin.version>3.14.0</maven.compiler.plugin.version>
119+
<maven.dependency.plugin.version>3.8.1</maven.dependency.plugin.version>
118120
<maven.deploy.plugin.version>3.1.4</maven.deploy.plugin.version>
119121
<maven.enforcer.plugin.version>3.5.0</maven.enforcer.plugin.version>
120122
<maven.gpg.plugin.version>3.2.7</maven.gpg.plugin.version>
@@ -136,6 +138,7 @@
136138
<com.github.spotbugs.version>4.9.3</com.github.spotbugs.version>
137139
<!-- Dependency versions -->
138140
<jakarta.validation-api.version>3.1.1</jakarta.validation-api.version>
141+
<jmh.version>1.37</jmh.version>
139142
<json.version>20250107</json.version>
140143
<junit-bom.version>5.12.1</junit-bom.version>
141144
<maven-surefire-junit5-tree-reporter.version>1.4.0</maven-surefire-junit5-tree-reporter.version>
@@ -175,6 +178,12 @@
175178
<scope>provided</scope>
176179
<optional>true</optional>
177180
</dependency>
181+
<dependency>
182+
<groupId>org.openjdk.jmh</groupId>
183+
<artifactId>jmh-core</artifactId>
184+
<version>${jmh.version}</version>
185+
<scope>test</scope>
186+
</dependency>
178187
<dependency>
179188
<groupId>org.json</groupId>
180189
<artifactId>json</artifactId>
@@ -265,10 +274,10 @@
265274
</pluginManagement>
266275
<plugins>
267276
<!--
268-
~ Parses the version into components.
269-
~
270-
~ The parsed version is used to generate the `Specification-Version` manifest header.
271-
-->
277+
~ Parses the version into components.
278+
~
279+
~ The parsed version is used to generate the `Specification-Version` manifest header.
280+
-->
272281
<plugin>
273282
<groupId>org.codehaus.mojo</groupId>
274283
<artifactId>build-helper-maven-plugin</artifactId>
@@ -318,16 +327,16 @@
318327
<arg>--should-stop=ifError=FLOW</arg>
319328
<arg>-Xplugin:ErrorProne</arg>
320329
<!--
321-
~ Due to a bug in IntelliJ IDEA, annotation processing MUST be enabled.
322-
~ Failing to do so will cause IDEA to ignore the annotation processor path
323-
~ and choke on the Error Prone compiler arguments.
324-
~
325-
~ On the other hand, we cannot pass an empty `annotationProcessors` list to Maven,
326-
~ since the `-processor` compiler argument requires at least one processor class name.
327-
~
328-
~ If you add an annotation processor, please also add an `annotationProcessors` configuration
329-
~ option.
330-
-->
330+
~ Due to a bug in IntelliJ IDEA, annotation processing MUST be enabled.
331+
~ Failing to do so will cause IDEA to ignore the annotation processor path
332+
~ and choke on the Error Prone compiler arguments.
333+
~
334+
~ On the other hand, we cannot pass an empty `annotationProcessors` list to Maven,
335+
~ since the `-processor` compiler argument requires at least one processor class name.
336+
~
337+
~ If you add an annotation processor, please also add an `annotationProcessors` configuration
338+
~ option.
339+
-->
331340
</compilerArgs>
332341
<annotationProcessorPaths>
333342
<path>
@@ -337,6 +346,26 @@
337346
</path>
338347
</annotationProcessorPaths>
339348
</configuration>
349+
<executions>
350+
<execution>
351+
<id>default-testCompile</id>
352+
<configuration>
353+
<compilerArgs combine.children="append">
354+
<arg>-proc:full</arg>
355+
</compilerArgs>
356+
<annotationProcessors>
357+
<processor>org.openjdk.jmh.generators.BenchmarkProcessor</processor>
358+
</annotationProcessors>
359+
<annotationProcessorPaths combine.children="append">
360+
<path>
361+
<groupId>org.openjdk.jmh</groupId>
362+
<artifactId>jmh-generator-annprocess</artifactId>
363+
<version>${jmh.version}</version>
364+
</path>
365+
</annotationProcessorPaths>
366+
</configuration>
367+
</execution>
368+
</executions>
340369
</plugin>
341370
<plugin>
342371
<groupId>com.diffplug.spotless</groupId>
@@ -602,5 +631,54 @@
602631
</plugins>
603632
</build>
604633
</profile>
634+
635+
<profile>
636+
<id>benchmark</id>
637+
<properties>
638+
<jmh.args>.*</jmh.args>
639+
<skipTests>true</skipTests>
640+
</properties>
641+
<build>
642+
<defaultGoal>test-compile
643+
dependency:build-classpath@build-classpath
644+
exec:exec@run-benchmark</defaultGoal>
645+
<plugins>
646+
<plugin>
647+
<groupId>org.apache.maven.plugins</groupId>
648+
<artifactId>maven-dependency-plugin</artifactId>
649+
<version>${maven.dependency.plugin.version}</version>
650+
<executions>
651+
<execution>
652+
<id>build-classpath</id>
653+
<goals>
654+
<goal>build-classpath</goal>
655+
</goals>
656+
<configuration>
657+
<includeScope>test</includeScope>
658+
<outputProperty>test.classpath</outputProperty>
659+
</configuration>
660+
</execution>
661+
</executions>
662+
</plugin>
663+
<plugin>
664+
<groupId>org.codehaus.mojo</groupId>
665+
<artifactId>exec-maven-plugin</artifactId>
666+
<version>${exec.maven.plugin.version}</version>
667+
<executions>
668+
<execution>
669+
<id>run-benchmark</id>
670+
<goals>
671+
<goal>exec</goal>
672+
</goals>
673+
<configuration>
674+
<executable>${java.home}/bin/java</executable>
675+
<commandlineArgs>-cp target/classes:target/test-classes:${test.classpath} org.openjdk.jmh.Main ${jmh.args}</commandlineArgs>
676+
</configuration>
677+
</execution>
678+
</executions>
679+
</plugin>
680+
</plugins>
681+
</build>
682+
</profile>
605683
</profiles>
606684
</project>

src/main/java/com/github/packageurl/PackageURL.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,8 @@ private static byte percentDecode(final byte[] bytes, final int start) {
630630
return ((byte) ((c1 << 4) + c2));
631631
}
632632

633-
private static String percentDecode(final String source) {
633+
// package-private for testing
634+
static String percentDecode(final String source) {
634635
if (source.isEmpty()) {
635636
return source;
636637
}
@@ -671,7 +672,8 @@ private static boolean isPercent(int c) {
671672
return (c == PERCENT_CHAR);
672673
}
673674

674-
private static String percentEncode(final String source) {
675+
// package-private for testing
676+
static String percentEncode(final String source) {
675677
if (source.isEmpty()) {
676678
return source;
677679
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package com.github.packageurl;
23+
24+
import java.nio.charset.StandardCharsets;
25+
import java.util.Random;
26+
import java.util.concurrent.TimeUnit;
27+
import org.openjdk.jmh.annotations.Benchmark;
28+
import org.openjdk.jmh.annotations.BenchmarkMode;
29+
import org.openjdk.jmh.annotations.Mode;
30+
import org.openjdk.jmh.annotations.OutputTimeUnit;
31+
import org.openjdk.jmh.annotations.Param;
32+
import org.openjdk.jmh.annotations.Scope;
33+
import org.openjdk.jmh.annotations.Setup;
34+
import org.openjdk.jmh.annotations.State;
35+
import org.openjdk.jmh.infra.Blackhole;
36+
37+
/**
38+
* Measures the performance of performance decoding and encoding.
39+
* <p>
40+
* Run the benchmark with:
41+
* </p>
42+
* <pre>
43+
* mvn -Pbenchmark
44+
* </pre>
45+
*/
46+
@BenchmarkMode(Mode.AverageTime)
47+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
48+
@State(Scope.Benchmark)
49+
public class PercentEncodingBenchmark {
50+
51+
private static final int DATA_COUNT = 1000;
52+
private static final int DECODED_LENGTH = 256;
53+
private static final byte[] UNRESERVED =
54+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~".getBytes(StandardCharsets.US_ASCII);
55+
56+
@Param({"0", "0.1", "0.5"})
57+
private double nonAsciiProb;
58+
59+
private String[] decodedData = createDecodedData();
60+
private String[] encodedData = encodeData(decodedData);
61+
62+
@Setup
63+
public void setup() {
64+
decodedData = createDecodedData();
65+
encodedData = encodeData(encodedData);
66+
}
67+
68+
private String[] createDecodedData() {
69+
Random random = new Random();
70+
String[] decodedData = new String[DATA_COUNT];
71+
for (int i = 0; i < DATA_COUNT; i++) {
72+
char[] chars = new char[DECODED_LENGTH];
73+
for (int j = 0; j < DECODED_LENGTH; j++) {
74+
if (random.nextDouble() < nonAsciiProb) {
75+
chars[j] = (char) (Byte.MAX_VALUE + 1 + random.nextInt(Short.MAX_VALUE - Byte.MAX_VALUE - 1));
76+
} else {
77+
chars[j] = (char) UNRESERVED[random.nextInt(UNRESERVED.length)];
78+
}
79+
}
80+
decodedData[i] = new String(chars);
81+
}
82+
return decodedData;
83+
}
84+
85+
private static String[] encodeData(String[] decodedData) {
86+
String[] encodedData = new String[decodedData.length];
87+
for (int i = 0; i < decodedData.length; i++) {
88+
encodedData[i] = PackageURL.percentEncode(decodedData[i]);
89+
}
90+
return encodedData;
91+
}
92+
93+
@Benchmark
94+
public void baseline(Blackhole blackhole) {
95+
for (int i = 0; i < DATA_COUNT; i++) {
96+
byte[] buffer = decodedData[i].getBytes(StandardCharsets.UTF_8);
97+
// Change the String a little bit
98+
for (int idx = 0; idx < buffer.length; idx++) {
99+
byte b = buffer[idx];
100+
if ('a' <= b && b <= 'z') {
101+
buffer[idx] = (byte) (b & 0x20);
102+
}
103+
}
104+
blackhole.consume(new String(buffer, StandardCharsets.UTF_8));
105+
}
106+
}
107+
108+
@Benchmark
109+
public void percentDecode(final Blackhole blackhole) {
110+
for (int i = 0; i < DATA_COUNT; i++) {
111+
blackhole.consume(PackageURL.percentDecode(encodedData[i]));
112+
}
113+
}
114+
115+
@Benchmark
116+
public void percentEncode(final Blackhole blackhole) {
117+
for (int i = 0; i < DATA_COUNT; i++) {
118+
blackhole.consume(PackageURL.percentEncode(decodedData[i]));
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)