Skip to content

Commit 647fbd3

Browse files
feat: add a small benchmark for percent encoding decoding (#217)
* 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>"`. * Apply suggestions from code review Co-authored-by: Jeremy Long <jeremy.long@gmail.com> --------- Co-authored-by: Jeremy Long <jeremy.long@gmail.com>
1 parent 3c63274 commit 647fbd3

File tree

3 files changed

+221
-14
lines changed

3 files changed

+221
-14
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,7 @@ private static byte percentDecode(final byte[] bytes, final int start) {
693693
return ((byte) ((c1 << 4) + c2));
694694
}
695695

696+
// package-private for testing
696697
static String percentDecode(final String source) {
697698
if (source.isEmpty()) {
698699
return source;
@@ -742,6 +743,7 @@ private static boolean isPercent(int c) {
742743
return (c == PERCENT_CHAR);
743744
}
744745

746+
// package-private for testing
745747
static String percentEncode(final String source) {
746748
if (source.isEmpty()) {
747749
return source;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
* <p>
46+
* To pass arguments to JMH use:
47+
* </p>
48+
* <pre>
49+
* mvn -Pbenchmark -Djmh.args="<arguments>"
50+
* </pre>
51+
*/
52+
@BenchmarkMode(Mode.AverageTime)
53+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
54+
@State(Scope.Benchmark)
55+
public class PercentEncodingBenchmark {
56+
57+
private static final int DATA_COUNT = 1000;
58+
private static final int DECODED_LENGTH = 256;
59+
private static final byte[] UNRESERVED =
60+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~".getBytes(StandardCharsets.US_ASCII);
61+
62+
@Param({"0", "0.1", "0.5"})
63+
private double nonAsciiProb;
64+
65+
private String[] decodedData = createDecodedData();
66+
private String[] encodedData = encodeData(decodedData);
67+
68+
@Setup
69+
public void setup() {
70+
decodedData = createDecodedData();
71+
encodedData = encodeData(encodedData);
72+
}
73+
74+
private String[] createDecodedData() {
75+
Random random = new Random();
76+
String[] decodedData = new String[DATA_COUNT];
77+
for (int i = 0; i < DATA_COUNT; i++) {
78+
char[] chars = new char[DECODED_LENGTH];
79+
for (int j = 0; j < DECODED_LENGTH; j++) {
80+
if (random.nextDouble() < nonAsciiProb) {
81+
chars[j] = (char) (Byte.MAX_VALUE + 1 + random.nextInt(Short.MAX_VALUE - Byte.MAX_VALUE - 1));
82+
} else {
83+
chars[j] = (char) UNRESERVED[random.nextInt(UNRESERVED.length)];
84+
}
85+
}
86+
decodedData[i] = new String(chars);
87+
}
88+
return decodedData;
89+
}
90+
91+
private static String[] encodeData(String[] decodedData) {
92+
String[] encodedData = new String[decodedData.length];
93+
for (int i = 0; i < decodedData.length; i++) {
94+
encodedData[i] = PackageURL.percentEncode(decodedData[i]);
95+
}
96+
return encodedData;
97+
}
98+
99+
@Benchmark
100+
public void baseline(Blackhole blackhole) {
101+
for (int i = 0; i < DATA_COUNT; i++) {
102+
byte[] buffer = decodedData[i].getBytes(StandardCharsets.UTF_8);
103+
// Change the String a little bit
104+
for (int idx = 0; idx < buffer.length; idx++) {
105+
byte b = buffer[idx];
106+
if ('a' <= b && b <= 'z') {
107+
buffer[idx] = (byte) (b & 0x20);
108+
}
109+
}
110+
blackhole.consume(new String(buffer, StandardCharsets.UTF_8));
111+
}
112+
}
113+
114+
@Benchmark
115+
public void percentDecode(final Blackhole blackhole) {
116+
for (int i = 0; i < DATA_COUNT; i++) {
117+
blackhole.consume(PackageURL.percentDecode(encodedData[i]));
118+
}
119+
}
120+
121+
@Benchmark
122+
public void percentEncode(final Blackhole blackhole) {
123+
for (int i = 0; i < DATA_COUNT; i++) {
124+
blackhole.consume(PackageURL.percentEncode(decodedData[i]));
125+
}
126+
}
127+
}

0 commit comments

Comments
 (0)