Skip to content

Commit 791954d

Browse files
dougqhclaude
andcommitted
Optimize template/matrix variable merging in spring-webmvc instrumentation
Replace eager HashMap copy + PairList allocations with a lazy MergedMapView that defers the merge until first iteration. Add empty-map short-circuits to skip merge logic entirely when template or matrix vars are empty (the common case). This reduces per-request allocations in the spring-webmvc AppSec path for both 3.1 and 6.0 instrumentations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3fb3733 commit 791954d

5 files changed

Lines changed: 252 additions & 30 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package datadog.trace.instrumentation.springweb;
2+
3+
import java.util.Collection;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
/**
9+
* A read-only Map view that lazily merges two maps. When both maps contain the same key, the values
10+
* are combined into a {@link PairList}. The actual merge is deferred until the first operation that
11+
* requires it (e.g. entrySet, size), avoiding unnecessary HashMap copies when the map is never
12+
* iterated.
13+
*/
14+
public final class MergedMapView implements Map<String, Object> {
15+
16+
private final Map<String, Object> primary;
17+
private final Map<String, Object> secondary;
18+
private Map<String, Object> merged;
19+
20+
public MergedMapView(Map<String, Object> primary, Map<String, Object> secondary) {
21+
this.primary = primary;
22+
this.secondary = secondary;
23+
}
24+
25+
private Map<String, Object> merged() {
26+
if (merged == null) {
27+
merged = new HashMap<>(primary);
28+
for (Map.Entry<String, Object> e : secondary.entrySet()) {
29+
String key = e.getKey();
30+
Object curValue = merged.get(key);
31+
if (curValue != null) {
32+
merged.put(key, new PairList(curValue, e.getValue()));
33+
} else {
34+
merged.put(key, e.getValue());
35+
}
36+
}
37+
}
38+
return merged;
39+
}
40+
41+
@Override
42+
public Object get(Object key) {
43+
Object v1 = primary.get(key);
44+
Object v2 = secondary.get(key);
45+
if (v1 != null && v2 != null) {
46+
return new PairList(v1, v2);
47+
}
48+
return v1 != null ? v1 : v2;
49+
}
50+
51+
@Override
52+
public boolean containsKey(Object key) {
53+
return primary.containsKey(key) || secondary.containsKey(key);
54+
}
55+
56+
@Override
57+
public int size() {
58+
return merged().size();
59+
}
60+
61+
@Override
62+
public boolean isEmpty() {
63+
return primary.isEmpty() && secondary.isEmpty();
64+
}
65+
66+
@Override
67+
public boolean containsValue(Object value) {
68+
return merged().containsValue(value);
69+
}
70+
71+
@Override
72+
public Object put(String key, Object value) {
73+
throw new UnsupportedOperationException();
74+
}
75+
76+
@Override
77+
public Object remove(Object key) {
78+
throw new UnsupportedOperationException();
79+
}
80+
81+
@Override
82+
public void putAll(Map<? extends String, ?> m) {
83+
throw new UnsupportedOperationException();
84+
}
85+
86+
@Override
87+
public void clear() {
88+
throw new UnsupportedOperationException();
89+
}
90+
91+
@Override
92+
public Set<String> keySet() {
93+
return merged().keySet();
94+
}
95+
96+
@Override
97+
public Collection<Object> values() {
98+
return merged().values();
99+
}
100+
101+
@Override
102+
public Set<Entry<String, Object>> entrySet() {
103+
return merged().entrySet();
104+
}
105+
106+
@Override
107+
public boolean equals(Object o) {
108+
return merged().equals(o);
109+
}
110+
111+
@Override
112+
public int hashCode() {
113+
return merged().hashCode();
114+
}
115+
116+
@Override
117+
public String toString() {
118+
return merged().toString();
119+
}
120+
}

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-3.1/src/main/java/datadog/trace/instrumentation/springweb/TemplateAndMatrixVariablesInstrumentation.java

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import datadog.trace.api.iast.propagation.PropagationModule;
2626
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
2727
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
28-
import java.util.HashMap;
2928
import java.util.Map;
3029
import java.util.Set;
3130
import java.util.function.BiFunction;
@@ -82,7 +81,7 @@ public void methodAdvice(MethodTransformer transformer) {
8281
@Override
8382
public String[] helperClassNames() {
8483
return new String[] {
85-
packageName + ".PairList",
84+
packageName + ".PairList", packageName + ".MergedMapView",
8685
};
8786
}
8887

@@ -136,23 +135,15 @@ public static void after(
136135

137136
// merge the uri template and matrix variables
138137
Map<String, Object> map = null;
139-
if (templateVars instanceof Map) {
138+
if (templateVars instanceof Map && !((Map<?, ?>) templateVars).isEmpty()) {
140139
map = (Map<String, Object>) templateVars;
141140
}
142-
if (matrixVars instanceof Map) {
141+
if (matrixVars instanceof Map && !((Map<?, ?>) matrixVars).isEmpty()) {
142+
Map<String, Object> matrixMap = (Map<String, Object>) matrixVars;
143143
if (map != null) {
144-
map = new HashMap<>(map);
145-
for (Map.Entry<String, Object> e : ((Map<String, Object>) matrixVars).entrySet()) {
146-
String key = e.getKey();
147-
Object curValue = map.get(key);
148-
if (curValue != null) {
149-
map.put(key, new PairList(curValue, e.getValue()));
150-
} else {
151-
map.put(key, e.getValue());
152-
}
153-
}
144+
map = new MergedMapView(map, matrixMap);
154145
} else {
155-
map = (Map<String, Object>) matrixVars;
146+
map = matrixMap;
156147
}
157148
}
158149

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java/datadog/trace/instrumentation/springweb6/TemplateAndMatrixVariablesInstrumentation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void methodAdvice(MethodTransformer transformer) {
6464
@Override
6565
public String[] helperClassNames() {
6666
return new String[] {
67-
packageName + ".PairList",
67+
packageName + ".PairList", packageName + ".MergedMapView",
6868
};
6969
}
7070

dd-java-agent/instrumentation/spring/spring-webmvc/spring-webmvc-6.0/src/main/java17/datadog/trace/instrumentation/springweb6/HandleMatchAdvice.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
1717
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
1818
import jakarta.servlet.http.HttpServletRequest;
19-
import java.util.HashMap;
2019
import java.util.Map;
2120
import java.util.function.BiFunction;
2221
import net.bytebuddy.asm.Advice;
@@ -66,23 +65,15 @@ public static void after(
6665

6766
// merge the uri template and matrix variables
6867
Map<String, Object> map = null;
69-
if (templateVars instanceof Map) {
68+
if (templateVars instanceof Map && !((Map<?, ?>) templateVars).isEmpty()) {
7069
map = (Map<String, Object>) templateVars;
7170
}
72-
if (matrixVars instanceof Map) {
71+
if (matrixVars instanceof Map && !((Map<?, ?>) matrixVars).isEmpty()) {
72+
Map<String, Object> matrixMap = (Map<String, Object>) matrixVars;
7373
if (map != null) {
74-
map = new HashMap<>(map);
75-
for (Map.Entry<String, Object> e : ((Map<String, Object>) matrixVars).entrySet()) {
76-
String key = e.getKey();
77-
Object curValue = map.get(key);
78-
if (curValue != null) {
79-
map.put(key, new PairList(curValue, e.getValue()));
80-
} else {
81-
map.put(key, e.getValue());
82-
}
83-
}
74+
map = new MergedMapView(map, matrixMap);
8475
} else {
85-
map = (Map<String, Object>) matrixVars;
76+
map = matrixMap;
8677
}
8778
}
8879

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package datadog.trace.instrumentation.springweb6;
2+
3+
import java.util.Collection;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
import java.util.Set;
7+
8+
/**
9+
* A read-only Map view that lazily merges two maps. When both maps contain the same key, the values
10+
* are combined into a {@link PairList}. The actual merge is deferred until the first operation that
11+
* requires it (e.g. entrySet, size), avoiding unnecessary HashMap copies when the map is never
12+
* iterated.
13+
*/
14+
public final class MergedMapView implements Map<String, Object> {
15+
16+
private final Map<String, Object> primary;
17+
private final Map<String, Object> secondary;
18+
private Map<String, Object> merged;
19+
20+
public MergedMapView(Map<String, Object> primary, Map<String, Object> secondary) {
21+
this.primary = primary;
22+
this.secondary = secondary;
23+
}
24+
25+
private Map<String, Object> merged() {
26+
if (merged == null) {
27+
merged = new HashMap<>(primary);
28+
for (Map.Entry<String, Object> e : secondary.entrySet()) {
29+
String key = e.getKey();
30+
Object curValue = merged.get(key);
31+
if (curValue != null) {
32+
merged.put(key, new PairList(curValue, e.getValue()));
33+
} else {
34+
merged.put(key, e.getValue());
35+
}
36+
}
37+
}
38+
return merged;
39+
}
40+
41+
@Override
42+
public Object get(Object key) {
43+
Object v1 = primary.get(key);
44+
Object v2 = secondary.get(key);
45+
if (v1 != null && v2 != null) {
46+
return new PairList(v1, v2);
47+
}
48+
return v1 != null ? v1 : v2;
49+
}
50+
51+
@Override
52+
public boolean containsKey(Object key) {
53+
return primary.containsKey(key) || secondary.containsKey(key);
54+
}
55+
56+
@Override
57+
public int size() {
58+
return merged().size();
59+
}
60+
61+
@Override
62+
public boolean isEmpty() {
63+
return primary.isEmpty() && secondary.isEmpty();
64+
}
65+
66+
@Override
67+
public boolean containsValue(Object value) {
68+
return merged().containsValue(value);
69+
}
70+
71+
@Override
72+
public Object put(String key, Object value) {
73+
throw new UnsupportedOperationException();
74+
}
75+
76+
@Override
77+
public Object remove(Object key) {
78+
throw new UnsupportedOperationException();
79+
}
80+
81+
@Override
82+
public void putAll(Map<? extends String, ?> m) {
83+
throw new UnsupportedOperationException();
84+
}
85+
86+
@Override
87+
public void clear() {
88+
throw new UnsupportedOperationException();
89+
}
90+
91+
@Override
92+
public Set<String> keySet() {
93+
return merged().keySet();
94+
}
95+
96+
@Override
97+
public Collection<Object> values() {
98+
return merged().values();
99+
}
100+
101+
@Override
102+
public Set<Entry<String, Object>> entrySet() {
103+
return merged().entrySet();
104+
}
105+
106+
@Override
107+
public boolean equals(Object o) {
108+
return merged().equals(o);
109+
}
110+
111+
@Override
112+
public int hashCode() {
113+
return merged().hashCode();
114+
}
115+
116+
@Override
117+
public String toString() {
118+
return merged().toString();
119+
}
120+
}

0 commit comments

Comments
 (0)