Skip to content

Commit 95140c8

Browse files
authored
Fix benchmark configuration and isolated parser class loading (#2629)
1 parent 7739c08 commit 95140c8

6 files changed

Lines changed: 153 additions & 41 deletions

File tree

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,8 @@ jmh {
710710
, "-XX:+DebugNonSafepoints"
711711
]
712712

713-
profilers = ['async:libPath=/opt/async-profiler/lib/libasyncProfiler.so;output=tree;dir=build/reports/jmh']
713+
// Profiling is opt-in because the native library path depends on the host.
714+
profilers = providers.gradleProperty('jmhProfiler').map { [it] }.getOrElse([])
714715

715716
includes = ['.*JSQLParserBenchmark.*']
716717
warmupIterations = 2

src/test/java/net/sf/jsqlparser/benchmark/DynamicParserRunner.java

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,56 @@
99
*/
1010
package net.sf.jsqlparser.benchmark;
1111

12-
import net.sf.jsqlparser.parser.CCJSqlParser;
13-
import net.sf.jsqlparser.statement.Statements;
14-
12+
import java.lang.reflect.InvocationTargetException;
1513
import java.lang.reflect.Method;
1614
import java.net.URLClassLoader;
15+
import java.util.EnumMap;
16+
import java.util.Map;
1717
import java.util.concurrent.ExecutorService;
1818
import java.util.function.Consumer;
1919

20+
/** Owns the isolated class loader and keeps its AST types behind an Object boundary. */
2021
public class DynamicParserRunner implements SqlParserRunner {
22+
private final URLClassLoader loader;
2123
private final Method parseStatementsMethod;
24+
private final Map<Configuration, Consumer<Object>> configurations =
25+
new EnumMap<>(Configuration.class);
2226

2327
public DynamicParserRunner(URLClassLoader loader) throws Exception {
28+
this.loader = loader;
2429
Class<?> utilClass = loader.loadClass("net.sf.jsqlparser.parser.CCJSqlParserUtil");
25-
Class<?> ccjClass = loader.loadClass("net.sf.jsqlparser.parser.CCJSqlParser");
26-
Class<?> consumerClass = Class.forName("java.util.function.Consumer"); // interface OK
27-
parseStatementsMethod = utilClass.getMethod(
28-
"parseStatements",
29-
String.class,
30-
ExecutorService.class,
31-
consumerClass);
30+
Class<?> parserClass = loader.loadClass("net.sf.jsqlparser.parser.CCJSqlParser");
31+
parseStatementsMethod = utilClass.getMethod("parseStatements", String.class,
32+
ExecutorService.class, Consumer.class);
33+
for (Configuration configuration : Configuration.values()) {
34+
Method method = parserClass.getMethod(configuration.methodName, boolean.class);
35+
configurations.put(configuration, parser -> {
36+
try {
37+
method.invoke(parser, configuration.value);
38+
} catch (ReflectiveOperationException ex) {
39+
throw new IllegalStateException(
40+
"Cannot apply parser configuration " + configuration, ex);
41+
}
42+
});
43+
}
44+
}
45+
46+
@Override
47+
public Object parseStatements(String sql, ExecutorService executorService,
48+
Configuration configuration) throws Exception {
49+
try {
50+
return parseStatementsMethod.invoke(null, sql, executorService,
51+
configuration == null ? null : configurations.get(configuration));
52+
} catch (InvocationTargetException ex) {
53+
if (ex.getCause() instanceof Exception) {
54+
throw (Exception) ex.getCause();
55+
}
56+
throw ex;
57+
}
3258
}
3359

3460
@Override
35-
public Statements parseStatements(String sql,
36-
ExecutorService executorService,
37-
Consumer<CCJSqlParser> consumer) throws Exception {
38-
return (Statements) parseStatementsMethod.invoke(null, sql, executorService, null);
61+
public void close() throws java.io.IOException {
62+
loader.close();
3963
}
4064
}

src/test/java/net/sf/jsqlparser/benchmark/JSQLParserBenchmark.java

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
*/
1010
package net.sf.jsqlparser.benchmark;
1111

12-
import net.sf.jsqlparser.parser.CCJSqlParser;
13-
import net.sf.jsqlparser.statement.Statements;
1412
import org.openjdk.jmh.annotations.*;
1513
import org.openjdk.jmh.infra.Blackhole;
1614

@@ -21,7 +19,6 @@
2119
import java.nio.charset.StandardCharsets;
2220
import java.nio.file.*;
2321
import java.util.concurrent.*;
24-
import java.util.function.Consumer;
2522

2623
@BenchmarkMode(Mode.AverageTime)
2724
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@@ -44,13 +41,33 @@ public void setup() throws Exception {
4441
} else {
4542
Path jarPath = downloadJsqlparserJar(version);
4643
URLClassLoader loader = new URLClassLoader(new URL[] {jarPath.toUri().toURL()}, null);
47-
runner = new DynamicParserRunner(loader);
44+
try {
45+
runner = new DynamicParserRunner(loader);
46+
} catch (Exception ex) {
47+
loader.close();
48+
throw ex;
49+
}
4850
}
4951

50-
// Adjust path as necessary based on where source root is during test execution
51-
Path path = Paths.get("src/test/resources/net/sf/jsqlparser/performance.sql");
52-
sqlContent = Files.readString(path, StandardCharsets.UTF_8);
53-
executorService = Executors.newSingleThreadExecutor();
52+
// Reject incompatible SQL/configurations before collecting measurements.
53+
try {
54+
Path path = Paths.get("src/test/resources/net/sf/jsqlparser/performance.sql");
55+
sqlContent = Files.readString(path, StandardCharsets.UTF_8);
56+
executorService = Executors.newSingleThreadExecutor();
57+
Object statements = runner.parseStatements(sqlContent, executorService,
58+
SqlParserRunner.Configuration.SIMPLE);
59+
if (statements == null) {
60+
throw new IllegalStateException("Parser " + version
61+
+ " returned no statements for the benchmark corpus with SIMPLE configuration");
62+
}
63+
} catch (Exception ex) {
64+
try {
65+
tearDown();
66+
} catch (Exception cleanup) {
67+
ex.addSuppressed(cleanup);
68+
}
69+
throw ex;
70+
}
5471
}
5572

5673
private Path downloadJsqlparserJar(String version) throws IOException {
@@ -74,10 +91,10 @@ private Path downloadJsqlparserJar(String version) throws IOException {
7491

7592
@Benchmark
7693
public void parseSQLStatements(Blackhole blackhole) throws Exception {
77-
final Statements statements = runner.parseStatements(
94+
final Object statements = runner.parseStatements(
7895
sqlContent,
7996
executorService,
80-
(Consumer<CCJSqlParser>) parser -> parser.withAllowComplexParsing(false));
97+
SqlParserRunner.Configuration.SIMPLE);
8198
blackhole.consume(statements);
8299
}
83100

@@ -87,15 +104,23 @@ public void parseQuotedText(Blackhole blackhole) throws Exception {
87104
+ "INSERT INTO recycle_record (a,f) VALUES ('\\'anything', 'abc');\n"
88105
+ "INSERT INTO recycle_record (a,f) VALUES ('\\'','83653692186728700711687663398101');\n";
89106

90-
final Statements statements = runner.parseStatements(
107+
final Object statements = runner.parseStatements(
91108
sqlStr,
92109
executorService,
93-
(Consumer<CCJSqlParser>) parser -> parser.withBackslashEscapeCharacter(true));
110+
SqlParserRunner.Configuration.BACKSLASH_ESCAPES);
94111
blackhole.consume(statements);
95112
}
96113

97114
@TearDown(Level.Trial)
98-
public void tearDown() {
99-
executorService.shutdown();
115+
public void tearDown() throws Exception {
116+
try {
117+
if (executorService != null) {
118+
executorService.shutdownNow();
119+
}
120+
} finally {
121+
if (runner != null) {
122+
runner.close();
123+
}
124+
}
100125
}
101126
}

src/test/java/net/sf/jsqlparser/benchmark/LatestClasspathRunner.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@
99
*/
1010
package net.sf.jsqlparser.benchmark;
1111

12-
import net.sf.jsqlparser.parser.CCJSqlParser;
13-
import net.sf.jsqlparser.statement.Statements;
1412

1513
import java.util.concurrent.ExecutorService;
16-
import java.util.function.Consumer;
1714

1815
public class LatestClasspathRunner implements SqlParserRunner {
1916

2017
@Override
21-
public Statements parseStatements(String sql,
18+
public Object parseStatements(String sql,
2219
ExecutorService executorService,
23-
Consumer<CCJSqlParser> consumer) throws Exception {
20+
Configuration configuration) throws Exception {
2421
return net.sf.jsqlparser.parser.CCJSqlParserUtil.parseStatements(sql, executorService,
25-
consumer);
22+
configuration == null ? null : configuration.currentParserConfiguration);
2623
}
2724
}
2825

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2025 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.benchmark;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import java.net.URL;
14+
import java.net.URLClassLoader;
15+
import java.util.concurrent.Executors;
16+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
17+
import org.junit.jupiter.api.Test;
18+
import net.sf.jsqlparser.benchmark.SqlParserRunner.Configuration;
19+
20+
class ParserRunnerTest {
21+
@Test
22+
void isolatedParserReturnsItsOwnAstAndAppliesTheSameOptions() throws Exception {
23+
URL classes = CCJSqlParserUtil.class.getProtectionDomain().getCodeSource().getLocation();
24+
var executor = Executors.newSingleThreadExecutor();
25+
try (var current = new LatestClasspathRunner();
26+
var isolated =
27+
new DynamicParserRunner(new URLClassLoader(new URL[] {classes}, null))) {
28+
String sql = "SELECT 'it\\'s' AS value";
29+
Object expected =
30+
current.parseStatements(sql, executor, Configuration.BACKSLASH_ESCAPES);
31+
Object actual =
32+
isolated.parseStatements(sql, executor, Configuration.BACKSLASH_ESCAPES);
33+
assertEquals(expected.toString(), actual.toString());
34+
assertNotSame(expected.getClass(), actual.getClass());
35+
assertEquals(expected.getClass().getName(), actual.getClass().getName());
36+
assertEquals(
37+
current.parseStatements("SELECT 1", executor, Configuration.SIMPLE).toString(),
38+
isolated.parseStatements("SELECT 1", executor, Configuration.SIMPLE)
39+
.toString());
40+
assertThrows(Exception.class,
41+
() -> isolated.parseStatements("SELECT FROM", executor, Configuration.SIMPLE));
42+
assertFalse(executor.isShutdown());
43+
} finally {
44+
executor.shutdownNow();
45+
}
46+
}
47+
}

src/test/java/net/sf/jsqlparser/benchmark/SqlParserRunner.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,31 @@
99
*/
1010
package net.sf.jsqlparser.benchmark;
1111

12-
import net.sf.jsqlparser.parser.CCJSqlParser;
13-
import net.sf.jsqlparser.statement.Statements;
14-
1512
import java.util.concurrent.ExecutorService;
1613
import java.util.function.Consumer;
14+
import net.sf.jsqlparser.parser.CCJSqlParser;
15+
16+
public interface SqlParserRunner extends AutoCloseable {
17+
enum Configuration {
18+
SIMPLE("withAllowComplexParsing", false,
19+
parser -> parser.withAllowComplexParsing(false)), BACKSLASH_ESCAPES(
20+
"withBackslashEscapeCharacter", true,
21+
parser -> parser.withBackslashEscapeCharacter(true));
22+
23+
final String methodName;
24+
final boolean value;
25+
final Consumer<CCJSqlParser> currentParserConfiguration;
26+
27+
Configuration(String methodName, boolean value, Consumer<CCJSqlParser> configuration) {
28+
this.methodName = methodName;
29+
this.value = value;
30+
this.currentParserConfiguration = configuration;
31+
}
32+
}
33+
34+
Object parseStatements(String sql, ExecutorService executorService,
35+
Configuration configuration) throws Exception;
1736

18-
public interface SqlParserRunner {
19-
Statements parseStatements(String sql, ExecutorService executorService,
20-
Consumer<CCJSqlParser> consumer) throws Exception;
37+
@Override
38+
default void close() throws Exception {}
2139
}

0 commit comments

Comments
 (0)