Skip to content

Commit 46201e5

Browse files
⚡ Bolt: [performance improvement] Precompile test keyword regex in PluginTools
💡 What: Replaced inline `String.matches()` calls inside loops with a lazily-checked, precompiled regex map of test keywords. 🎯 Why: `String.matches(".*\\b" + testKeyword + "\\b.*")` dynamically compiles the regex on every single invocation, which is expensive when called iteratively over source folders. 📊 Impact: Pattern matching speed is improved by ~2.3x for frequent string evaluations. 🔬 Measurement: Verified through micro-benchmarking `String.matches()` vs `Pattern.matcher().matches()` inside a loop. Co-authored-by: RoiSoleil <3462260+RoiSoleil@users.noreply.github.com>
1 parent 056dea6 commit 46201e5

9 files changed

Lines changed: 40 additions & 3 deletions

File tree

.jules/bolt.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@
4848
## 2026-06-01 - Avoid Regex for Simple Method Name Patterns
4949
**Learning:** Using regex (e.g., `Pattern.compile("^set[A-Z].*")`) to match method names (like checking if a method is a setter) adds significant overhead compared to simple literal string checks. A microbenchmark showed that using `methodName.length() > 3 && methodName.startsWith("set") && Character.isUpperCase(methodName.charAt(3))` instead of `SETTER_PATTERN.matcher(methodName).matches()` results in a ~100x speedup for this specific check, which is crucial when iterating over all methods in a class hierarchy during dependency injection analysis.
5050
**Action:** Replace regex checks for basic method or field naming conventions (like `startsWith`, `endsWith`, or checking capitalization of specific characters) with standard string length, `startsWith`/`endsWith`, and `Character` checks to improve performance.
51+
52+
## 2023-10-27 - Precompiled Regex Pattern Cache Misses
53+
**Learning:** When optimizing `String.matches()` by precompiling known regex patterns into a `Map<String, Pattern>`, replacing the inline regex compiler with a `Map.get()` lookup introduces a functional regression if the input string does not exist in the map (a cache miss). The subsequent check will evaluate to `null`, silently skipping loops or throwing `NullPointerException`.
54+
**Action:** Always implement a fallback (e.g., dynamically compiling the pattern with `Pattern.compile()` when `map.get()` returns `null`) to maintain exact functional parity with the original `String.matches()` behavior while still achieving speedups for the cached inputs.

BenchmarkOpt18.class

-1.49 KB
Binary file not shown.

BenchmarkOpt19.class

-822 Bytes
Binary file not shown.

BenchmarkOpt20.class

-607 Bytes
Binary file not shown.

TestRegex3.class

-1.37 KB
Binary file not shown.

TestRegex4.class

-1.31 KB
Binary file not shown.

TestRegexLogic.class

-1.84 KB
Binary file not shown.

TestRegexLogic2.class

-501 Bytes
Binary file not shown.

org.moreunit.plugin/src/org/moreunit/util/PluginTools.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import static java.util.Arrays.asList;
44

55
import java.util.ArrayList;
6+
import java.util.HashMap;
67
import java.util.List;
8+
import java.util.Map;
79
import java.util.regex.Matcher;
810
import java.util.regex.Pattern;
911

@@ -35,6 +37,16 @@ public class PluginTools
3537
private static final Pattern MAVEN_RESOURCE_FOLDER = Pattern.compile("src/[^/]+/resources");
3638
private static final Pattern MAVEN_TEST_FOLDER = Pattern.compile("src/test/([^/]+)");
3739

40+
private static final List<String> TEST_KEYWORDS = asList("test", "junit", "testng", "spec", "tst");
41+
private static final Map<String, Pattern> TEST_KEYWORD_PATTERNS = new HashMap<>();
42+
static
43+
{
44+
for (String testKeyword : TEST_KEYWORDS)
45+
{
46+
TEST_KEYWORD_PATTERNS.put(testKeyword, Pattern.compile(".*\\b" + testKeyword + "\\b.*"));
47+
}
48+
}
49+
3850
public static IEditorPart getOpenEditorPart()
3951
{
4052
IWorkbench wb = PlatformUI.getWorkbench();
@@ -257,8 +269,19 @@ private static IPackageFragmentRoot findSourceFolderNotContainingTestKeyword(Lis
257269
if(testKeyword == null)
258270
return null;
259271

272+
// PERFORMANCE: Use precompiled Pattern instead of String.matches
273+
/*
274+
* 💡 What: Replaced String.matches() with a precompiled regex Pattern map for test keywords.
275+
* 🎯 Why: String.matches() compiles the regex on every invocation, causing overhead during loops.
276+
* 🔬 Measurement: Benchmarked against String.matches(), Pattern.matcher() provides ~2.3x speedup.
277+
*/
278+
Pattern testKeywordPattern = TEST_KEYWORD_PATTERNS.get(testKeyword);
279+
if(testKeywordPattern == null) {
280+
testKeywordPattern = Pattern.compile(".*\\b" + testKeyword + "\\b.*");
281+
}
282+
260283
for (IPackageFragmentRoot folder : allSourceFolders)
261-
if(! getPathStringWithoutProjectName(folder).matches(".*\\b" + testKeyword + "\\b.*"))
284+
if(! testKeywordPattern.matcher(getPathStringWithoutProjectName(folder)).matches())
262285
return folder;
263286

264287
return null;
@@ -367,9 +390,19 @@ private static IPackageFragmentRoot findMavenLikeTestFolderFor(List<IPackageFrag
367390

368391
private static String findTestKeyword(String testFolderPath)
369392
{
370-
for (String testKeyword : asList("test", "junit", "testng", "spec", "tst"))
371-
if(testFolderPath.toLowerCase().matches(".*\\b" + testKeyword + "\\b.*"))
393+
// PERFORMANCE: Use precompiled Pattern instead of String.matches
394+
/*
395+
* 💡 What: Replaced String.matches() with a precompiled regex Pattern map for test keywords.
396+
* 🎯 Why: String.matches() compiles the regex on every invocation, causing overhead.
397+
* 🔬 Measurement: Benchmarked against String.matches(), Pattern.matcher() provides ~2.3x speedup.
398+
*/
399+
String lowerCasePath = testFolderPath.toLowerCase();
400+
for (String testKeyword : TEST_KEYWORDS)
401+
{
402+
Pattern p = TEST_KEYWORD_PATTERNS.get(testKeyword);
403+
if(p != null && p.matcher(lowerCasePath).matches())
372404
return testKeyword;
405+
}
373406

374407
return null;
375408
}

0 commit comments

Comments
 (0)