Skip to content

Commit 2a60a20

Browse files
committed
feat(agent): Optimize class exclusion with PrefixTrie
Replaced the linear search for class exclusions in `AppMapPackage` with a `PrefixTrie` for `O(M)` lookup performance, where M is the length of the class name. This significantly improves performance, especially with large exclusion lists. Exclusion patterns can now be specified relative to the package path in `appmap.yml`, improving configuration clarity. Backward compatibility is maintained by supporting both relative and fully qualified exclusion patterns. The original `exclude` array was preserved for debugging and logging purposes to prevent breaking existing functionality in `AppMapConfig`.
1 parent 00afec4 commit 2a60a20

File tree

2 files changed

+96
-24
lines changed

2 files changed

+96
-24
lines changed

agent/src/main/java/com/appland/appmap/config/AppMapPackage.java

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import com.fasterxml.jackson.annotation.JsonCreator;
1212
import com.fasterxml.jackson.annotation.JsonProperty;
1313

14+
import com.appland.appmap.util.PrefixTrie;
15+
1416
import javassist.CtBehavior;
1517
public class AppMapPackage {
1618
private static final TaggedLogger logger = AppMapConfig.getLogger(null);
@@ -20,6 +22,7 @@ public class AppMapPackage {
2022
public String[] exclude = new String[] {};
2123
public boolean shallow = false;
2224
public Boolean allMethods = true;
25+
private final PrefixTrie excludeTrie = new PrefixTrie();
2326

2427
@JsonCreator
2528
public AppMapPackage(@JsonProperty("path") String path,
@@ -29,7 +32,20 @@ public AppMapPackage(@JsonProperty("path") String path,
2932
this.path = path;
3033
this.exclude = exclude == null ? new String[] {} : exclude;
3134
this.shallow = shallow != null && shallow;
32-
this.allMethods = allMethods == null ? true : allMethods;
35+
this.allMethods = allMethods == null || allMethods;
36+
37+
if (exclude != null) {
38+
final String packagePrefix = this.path + ".";
39+
for (String exclusion : exclude) {
40+
if (exclusion.startsWith(packagePrefix)) {
41+
// Absolute path, strip the package path and add the rest
42+
this.excludeTrie.insert(exclusion.substring(packagePrefix.length()));
43+
} else {
44+
// Relative path, add as-is
45+
this.excludeTrie.insert(exclusion);
46+
}
47+
}
48+
}
3349
}
3450

3551
public static class LabelConfig {
@@ -77,7 +93,7 @@ public boolean matches(String className, String methodName) {
7793

7894
/**
7995
* Check if a class/method is included in the configuration.
80-
*
96+
*
8197
* @param canonicalName the canonical name of the class/method to be checked
8298
* @return {@code true} if the class/method is included in the configuration. {@code false} if it
8399
* is not included or otherwise explicitly excluded.
@@ -119,39 +135,37 @@ public LabelConfig find(FullyQualifiedName canonicalName) {
119135
return null;
120136
}
121137

138+
private String getRelativeClassName(String fqcn) {
139+
final String packagePrefix = this.path + ".";
140+
if (fqcn.startsWith(packagePrefix)) {
141+
return fqcn.substring(packagePrefix.length());
142+
}
143+
return fqcn;
144+
}
145+
122146
/**
123147
* Checks whether the behavior is explicitly excluded
124148
*
125149
* @param behavior the behavior to be checked
126150
* @return {@code true} if the behavior is excluded
127151
*/
128152
public Boolean excludes(CtBehavior behavior) {
129-
final String fqClass = behavior.getDeclaringClass().getName();
130-
String candidateName = null;
131-
for (String exclusion : this.exclude) {
132-
if (fqClass.startsWith(exclusion)) {
133-
return true;
134-
} else {
135-
if (candidateName == null) {
136-
candidateName = fqClass + "." + behavior.getName();
137-
}
138-
139-
if (candidateName.startsWith(exclusion.replace('#', '.'))) {
140-
return true;
141-
}
142-
}
153+
String fqClass = behavior.getDeclaringClass().getName();
154+
String relativeClassName = getRelativeClassName(fqClass);
155+
if (this.excludeTrie.startsWith(relativeClassName)) {
156+
return true;
143157
}
144158

145-
return false;
159+
// Also check method-specific exclusions
160+
String methodName = behavior.getName();
161+
String relativeMethodName = String.format("%s.%s", relativeClassName, methodName)
162+
.replace('#', '.');
163+
return this.excludeTrie.startsWith(relativeMethodName);
146164
}
147165

148166
public Boolean excludes(FullyQualifiedName canonicalName) {
149-
for (String exclusion : this.exclude) {
150-
if (canonicalName.toString().startsWith(exclusion)) {
151-
return true;
152-
}
153-
}
154-
155-
return false;
167+
String fqcn = canonicalName.toString();
168+
String relativeName = getRelativeClassName(fqcn);
169+
return this.excludeTrie.startsWith(relativeName);
156170
}
157171
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.appland.appmap.util;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
/**
7+
* A simple Trie (Prefix Tree) for efficient prefix-based string matching.
8+
* This is used to check if a class name matches any of the exclusion patterns.
9+
*/
10+
public class PrefixTrie {
11+
private static class TrieNode {
12+
Map<Character, TrieNode> children = new HashMap<>();
13+
boolean isEndOfWord = false;
14+
}
15+
16+
private final TrieNode root;
17+
18+
public PrefixTrie() {
19+
root = new TrieNode();
20+
}
21+
22+
/**
23+
* Inserts a word into the Trie.
24+
* @param word The word to insert.
25+
*/
26+
public void insert(String word) {
27+
TrieNode current = root;
28+
for (char ch : word.toCharArray()) {
29+
current = current.children.computeIfAbsent(ch, c -> new TrieNode());
30+
}
31+
current.isEndOfWord = true;
32+
}
33+
34+
/**
35+
* Checks if any prefix of the given word exists in the Trie.
36+
* For example, if "java." is in the Trie, this will return true for "java.lang.String".
37+
* @param word The word to check.
38+
* @return {@code true} if a prefix of the word is found in the Trie, {@code false} otherwise.
39+
*/
40+
public boolean startsWith(String word) {
41+
TrieNode current = root;
42+
for (int i = 0; i < word.length(); i++) {
43+
char ch = word.charAt(i);
44+
current = current.children.get(ch);
45+
if (current == null) {
46+
return false; // No prefix match
47+
}
48+
if (current.isEndOfWord) {
49+
// We've found a stored pattern that is a prefix of the word.
50+
// e.g., Trie has "java." and word is "java.lang.String"
51+
return true;
52+
}
53+
}
54+
// The word itself is a prefix or an exact match for a pattern in the Trie
55+
// e.g., Trie has "java.lang" and word is "java.lang"
56+
return current.isEndOfWord;
57+
}
58+
}

0 commit comments

Comments
 (0)