Skip to content

Commit e9556bb

Browse files
anuragdyvy
andauthored
Optimize DefaultThreadContextMap.getCopy() performance (#3939)
Co-authored-by: Volkan Yazıcı <volkan@yazi.ci>
1 parent 8a3fb53 commit e9556bb

File tree

4 files changed

+165
-1
lines changed

4 files changed

+165
-1
lines changed

log4j-api-test/src/test/java/org/apache/logging/log4j/spi/DefaultThreadContextMapTest.java

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
*/
1717
package org.apache.logging.log4j.spi;
1818

19+
import static org.assertj.core.api.Assertions.assertThat;
1920
import static org.junit.jupiter.api.Assertions.assertEquals;
2021
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertNotSame;
2123
import static org.junit.jupiter.api.Assertions.assertTrue;
2224

2325
import java.util.HashMap;
@@ -130,4 +132,124 @@ void threadLocalNotInheritableByDefault() {
130132
void threadLocalInheritableIfConfigured() {
131133
threadLocalInheritableIfConfigured(createInheritableThreadContextMap());
132134
}
135+
136+
@Test
137+
void testGetCopyWithEmptyMap() {
138+
final DefaultThreadContextMap contextMap = new DefaultThreadContextMap();
139+
140+
// Verify map is empty
141+
assertTrue(contextMap.isEmpty());
142+
assertEquals(0, contextMap.size());
143+
144+
// Get copy of empty map
145+
final Map<String, String> copy = contextMap.getCopy();
146+
147+
// Verify copy is empty HashMap
148+
assertThat(copy).isInstanceOf(HashMap.class);
149+
assertTrue(copy.isEmpty());
150+
assertEquals(0, copy.size());
151+
152+
// Verify copy is independent
153+
copy.put("test", "value");
154+
assertTrue(contextMap.isEmpty());
155+
}
156+
157+
@Test
158+
void testGetCopyWithSingleElement() {
159+
final DefaultThreadContextMap contextMap = new DefaultThreadContextMap();
160+
161+
// Add single element
162+
contextMap.put("key1", "value1");
163+
assertEquals(1, contextMap.size());
164+
assertEquals("value1", contextMap.get("key1"));
165+
166+
// Get copy
167+
final Map<String, String> copy = contextMap.getCopy();
168+
169+
// Verify copy contains identical data
170+
assertThat(copy).isInstanceOf(HashMap.class);
171+
assertEquals(1, copy.size());
172+
assertEquals("value1", copy.get("key1"));
173+
assertTrue(copy.containsKey("key1"));
174+
175+
// Verify copy is independent
176+
assertNotSame(copy, contextMap.toMap());
177+
copy.put("key2", "value2");
178+
assertEquals(1, contextMap.size());
179+
assertFalse(contextMap.containsKey("key2"));
180+
}
181+
182+
@Test
183+
void testGetCopyWithMultipleElements() {
184+
final DefaultThreadContextMap contextMap = new DefaultThreadContextMap();
185+
186+
// Add multiple elements
187+
final Map<String, String> testData = new HashMap<>();
188+
testData.put("key1", "value1");
189+
testData.put("key2", "value2");
190+
testData.put("key3", "value3");
191+
testData.put("key4", "value4");
192+
testData.put("key5", "value5");
193+
194+
contextMap.putAll(testData);
195+
assertEquals(5, contextMap.size());
196+
197+
// Get copy
198+
final Map<String, String> copy = contextMap.getCopy();
199+
200+
// Verify copy contains identical data
201+
assertThat(copy).isInstanceOf(HashMap.class);
202+
assertEquals(5, copy.size());
203+
204+
// Verify all entries match
205+
assertEquals(testData, copy);
206+
207+
// Verify copy is independent
208+
copy.clear();
209+
assertEquals(5, contextMap.size());
210+
}
211+
212+
@Test
213+
void testGetCopyReturnsHashMap() {
214+
final DefaultThreadContextMap contextMap = new DefaultThreadContextMap();
215+
216+
// Test with empty map
217+
Map<String, String> copy = contextMap.getCopy();
218+
assertThat(copy).isInstanceOf(HashMap.class);
219+
220+
// Test with populated map
221+
contextMap.put("key", "value");
222+
copy = contextMap.getCopy();
223+
assertThat(copy).isInstanceOf(HashMap.class);
224+
}
225+
226+
@Test
227+
void testGetCopyIndependence() {
228+
final DefaultThreadContextMap contextMap = new DefaultThreadContextMap();
229+
230+
// Setup initial data
231+
contextMap.put("key1", "value1");
232+
contextMap.put("key2", "value2");
233+
234+
final Map<String, String> copy1 = contextMap.getCopy();
235+
final Map<String, String> copy2 = contextMap.getCopy();
236+
237+
// Verify copies are independent of each other
238+
assertNotSame(copy1, copy2);
239+
assertEquals(copy1, copy2);
240+
241+
// Modify first copy
242+
copy1.put("key3", "value3");
243+
copy1.remove("key1");
244+
245+
// Verify second copy is unaffected
246+
assertEquals(2, copy2.size());
247+
assertTrue(copy2.containsKey("key1"));
248+
assertFalse(copy2.containsKey("key3"));
249+
250+
// Verify original map is unaffected
251+
assertEquals(2, contextMap.size());
252+
assertTrue(contextMap.containsKey("key1"));
253+
assertFalse(contextMap.containsKey("key3"));
254+
}
133255
}

log4j-api/src/main/java/org/apache/logging/log4j/spi/DefaultThreadContextMap.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,37 @@ public <V> V getValue(final String key) {
147147
return (V) get(key);
148148
}
149149

150+
/**
151+
* {@return a mutable copy of the current thread context map}
152+
*/
150153
@Override
151154
public Map<String, String> getCopy() {
152155
final Object[] state = localState.get();
153156
if (state == null) {
154157
return new HashMap<>(0);
155158
}
156-
return new HashMap<>(getMap(state));
159+
160+
final Map<String, String> map = getMap(state);
161+
162+
// Handle empty map case efficiently - constructor is faster for empty maps
163+
if (map.isEmpty()) {
164+
return new HashMap<>();
165+
}
166+
167+
// Pre-size HashMap to minimize rehashing operations
168+
// Factor 1.35 accounts for HashMap's 0.75 load factor (1/0.75 ≈ 1.33)
169+
final HashMap<String, String> copy = new HashMap<>((int) (map.size() * 1.35));
170+
171+
// Manual iteration avoids megamorphic virtual calls that prevent JIT optimization.
172+
// The HashMap(Map) constructor requires (3 + 4n) virtual method calls that become
173+
// megamorphic when used with different map types, leading to 24-136% performance
174+
// degradation. Manual iteration creates monomorphic call sites that JIT can optimize.
175+
// See https://bugs.openjdk.org/browse/JDK-8368292
176+
for (final Map.Entry<String, String> entry : map.entrySet()) {
177+
copy.put(entry.getKey(), entry.getValue());
178+
}
179+
180+
return copy;
157181
}
158182

159183
@Override

log4j-perf-test/src/main/java/org/apache/logging/log4j/perf/jmh/ThreadContextBenchmark.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ public Map<String, String> legacyInjectWithProperties() {
167167
return createMap(propertyList);
168168
}
169169

170+
@Benchmark
171+
public Map<String, String> getCopy() {
172+
return ThreadContext.getContext();
173+
}
174+
170175
// from Log4jLogEvent::createMap
171176
static Map<String, String> createMap(final List<Property> properties) {
172177
final Map<String, String> contextMap = ThreadContext.getImmutableContext();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<entry xmlns="https://logging.apache.org/xml/ns"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="
5+
https://logging.apache.org/xml/ns
6+
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
7+
type="changed">
8+
<issue id="3935" link="https://github.com/apache/logging-log4j2/issues/3935"/>
9+
<issue id="3939" link="https://github.com/apache/logging-log4j2/pull/3939"/>
10+
<description format="asciidoc">
11+
Optimize `DefaultThreadContextMap.getCopy()` performance by avoiding megamorphic calls in `HashMap` constructor
12+
</description>
13+
</entry>

0 commit comments

Comments
 (0)