Skip to content

Commit 0607af8

Browse files
committed
Improve ConfigurationPropertySource performance
Further improve the performance of `containsDescendantOf` by using a Map to limit the number of candidates that need checking. Closes gh-21416
1 parent 4af6e7f commit 0607af8

File tree

4 files changed

+114
-18
lines changed

4 files changed

+114
-18
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ public ConfigurationPropertyName append(String elements) {
194194
return new ConfigurationPropertyName(this.elements.append(additionalElements));
195195
}
196196

197+
/**
198+
* Return the parent of this {@link ConfigurationPropertyName} or
199+
* {@link ConfigurationPropertyName#EMPTY} if there is no parent.
200+
* @return the parent name
201+
*/
202+
public ConfigurationPropertyName getParent() {
203+
int numberOfElements = getNumberOfElements();
204+
return (numberOfElements <= 1) ? EMPTY : chop(numberOfElements - 1);
205+
}
206+
197207
/**
198208
* Return a new {@link ConfigurationPropertyName} by chopping this name to the given
199209
* {@code size}. For example, {@code chop(1)} on the name {@code foo.bar} will return

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020
import java.util.Collections;
2121
import java.util.ConcurrentModificationException;
2222
import java.util.HashMap;
23+
import java.util.HashSet;
2324
import java.util.Iterator;
24-
import java.util.List;
2525
import java.util.Map;
2626
import java.util.NoSuchElementException;
2727
import java.util.Objects;
28+
import java.util.Set;
2829
import java.util.function.BiPredicate;
2930
import java.util.function.Supplier;
3031
import java.util.stream.Stream;
@@ -37,8 +38,6 @@
3738
import org.springframework.core.env.PropertySource;
3839
import org.springframework.core.env.StandardEnvironment;
3940
import org.springframework.core.env.SystemEnvironmentPropertySource;
40-
import org.springframework.util.LinkedMultiValueMap;
41-
import org.springframework.util.MultiValueMap;
4241

4342
/**
4443
* {@link ConfigurationPropertySource} backed by an {@link EnumerablePropertySource}.
@@ -54,17 +53,17 @@
5453
class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource
5554
implements IterableConfigurationPropertySource, CachingConfigurationPropertySource {
5655

57-
private volatile ConfigurationPropertyName[] configurationPropertyNames;
56+
private final BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> ancestorOfCheck;
5857

5958
private final SoftReferenceConfigurationPropertyCache<Mappings> cache;
6059

61-
private final BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> ancestorOfCheck;
60+
private volatile ConfigurationPropertyName[] configurationPropertyNames;
6261

6362
SpringIterableConfigurationPropertySource(EnumerablePropertySource<?> propertySource, PropertyMapper... mappers) {
6463
super(propertySource, mappers);
6564
assertEnumerablePropertySource();
66-
this.cache = new SoftReferenceConfigurationPropertyCache<>(isImmutablePropertySource());
6765
this.ancestorOfCheck = getAncestorOfCheck(mappers);
66+
this.cache = new SoftReferenceConfigurationPropertyCache<>(isImmutablePropertySource());
6867
}
6968

7069
private BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> getAncestorOfCheck(
@@ -129,6 +128,9 @@ public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName
129128
if (result != ConfigurationPropertyState.UNKNOWN) {
130129
return result;
131130
}
131+
if (this.ancestorOfCheck == PropertyMapper.DEFAULT_ANCESTOR_OF_CHECK) {
132+
return getMappings().containsDescendantOf(name, this.ancestorOfCheck);
133+
}
132134
ConfigurationPropertyName[] candidates = getConfigurationPropertyNames();
133135
for (ConfigurationPropertyName candidate : candidates) {
134136
if (candidate != null && this.ancestorOfCheck.test(name, candidate)) {
@@ -156,7 +158,8 @@ private Mappings getMappings() {
156158
}
157159

158160
private Mappings createMappings() {
159-
return new Mappings(getMappers(), isImmutablePropertySource());
161+
return new Mappings(getMappers(), isImmutablePropertySource(),
162+
this.ancestorOfCheck == PropertyMapper.DEFAULT_ANCESTOR_OF_CHECK);
160163
}
161164

162165
private Mappings updateMappings(Mappings mappings) {
@@ -188,17 +191,22 @@ private static class Mappings {
188191

189192
private final boolean immutable;
190193

191-
private volatile MultiValueMap<ConfigurationPropertyName, String> mappings;
194+
private final boolean trackDescendants;
195+
196+
private volatile Map<ConfigurationPropertyName, Set<String>> mappings;
192197

193198
private volatile Map<String, ConfigurationPropertyName> reverseMappings;
194199

200+
private volatile Map<ConfigurationPropertyName, Set<ConfigurationPropertyName>> descendants;
201+
195202
private volatile ConfigurationPropertyName[] configurationPropertyNames;
196203

197204
private volatile String[] lastUpdated;
198205

199-
Mappings(PropertyMapper[] mappers, boolean immutable) {
206+
Mappings(PropertyMapper[] mappers, boolean immutable, boolean trackDescendants) {
200207
this.mappers = mappers;
201208
this.immutable = immutable;
209+
this.trackDescendants = trackDescendants;
202210
}
203211

204212
void updateMappings(Supplier<String[]> propertyNames) {
@@ -223,32 +231,52 @@ private void updateMappings(String[] propertyNames) {
223231
if (lastUpdated != null && Arrays.equals(lastUpdated, propertyNames)) {
224232
return;
225233
}
226-
MultiValueMap<ConfigurationPropertyName, String> previousMappings = this.mappings;
227-
MultiValueMap<ConfigurationPropertyName, String> mappings = (previousMappings != null)
228-
? new LinkedMultiValueMap<>(previousMappings) : new LinkedMultiValueMap<>(propertyNames.length);
229-
Map<String, ConfigurationPropertyName> previousReverseMappings = this.reverseMappings;
230-
Map<String, ConfigurationPropertyName> reverseMappings = (previousReverseMappings != null)
231-
? new HashMap<>(previousReverseMappings) : new HashMap<>(propertyNames.length);
234+
int size = propertyNames.length;
235+
Map<ConfigurationPropertyName, Set<String>> mappings = cloneOrCreate(this.mappings, size);
236+
Map<String, ConfigurationPropertyName> reverseMappings = cloneOrCreate(this.reverseMappings, size);
237+
Map<ConfigurationPropertyName, Set<ConfigurationPropertyName>> descendants = cloneOrCreate(this.descendants,
238+
size);
232239
for (PropertyMapper propertyMapper : this.mappers) {
233240
for (String propertyName : propertyNames) {
234241
if (!reverseMappings.containsKey(propertyName)) {
235242
ConfigurationPropertyName configurationPropertyName = propertyMapper.map(propertyName);
236243
if (configurationPropertyName != null && !configurationPropertyName.isEmpty()) {
237-
mappings.add(configurationPropertyName, propertyName);
244+
add(mappings, configurationPropertyName, propertyName);
238245
reverseMappings.put(propertyName, configurationPropertyName);
246+
if (this.trackDescendants) {
247+
addParents(descendants, configurationPropertyName);
248+
}
239249
}
240250
}
241251
}
242252
}
243253
this.mappings = mappings;
244254
this.reverseMappings = reverseMappings;
255+
this.descendants = descendants;
245256
this.lastUpdated = this.immutable ? null : propertyNames;
246257
this.configurationPropertyNames = this.immutable
247258
? reverseMappings.values().toArray(new ConfigurationPropertyName[0]) : null;
248259
}
249260

250-
List<String> getMapped(ConfigurationPropertyName configurationPropertyName) {
251-
return this.mappings.getOrDefault(configurationPropertyName, Collections.emptyList());
261+
private <K, V> Map<K, V> cloneOrCreate(Map<K, V> source, int size) {
262+
return (source != null) ? new HashMap<>(source) : new HashMap<>(size);
263+
}
264+
265+
private void addParents(Map<ConfigurationPropertyName, Set<ConfigurationPropertyName>> descendants,
266+
ConfigurationPropertyName name) {
267+
ConfigurationPropertyName parent = name;
268+
while (!parent.isEmpty()) {
269+
add(descendants, parent, name);
270+
parent = parent.getParent();
271+
}
272+
}
273+
274+
private <K, T> void add(Map<K, Set<T>> map, K key, T value) {
275+
map.computeIfAbsent(key, (k) -> new HashSet<>()).add(value);
276+
}
277+
278+
Set<String> getMapped(ConfigurationPropertyName configurationPropertyName) {
279+
return this.mappings.getOrDefault(configurationPropertyName, Collections.emptySet());
252280
}
253281

254282
ConfigurationPropertyName[] getConfigurationPropertyNames(String[] propertyNames) {
@@ -267,6 +295,20 @@ ConfigurationPropertyName[] getConfigurationPropertyNames(String[] propertyNames
267295
return names;
268296
}
269297

298+
ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name,
299+
BiPredicate<ConfigurationPropertyName, ConfigurationPropertyName> ancestorOfCheck) {
300+
if (name.isEmpty() && !this.descendants.isEmpty()) {
301+
return ConfigurationPropertyState.PRESENT;
302+
}
303+
Set<ConfigurationPropertyName> candidates = this.descendants.getOrDefault(name, Collections.emptySet());
304+
for (ConfigurationPropertyName candidate : candidates) {
305+
if (ancestorOfCheck.test(name, candidate)) {
306+
return ConfigurationPropertyState.PRESENT;
307+
}
308+
}
309+
return ConfigurationPropertyState.ABSENT;
310+
}
311+
270312
}
271313

272314
/**

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,26 @@ void appendWhenElementNameIsNullShouldReturnName() {
414414
assertThat((Object) name.append(null)).isSameAs(name);
415415
}
416416

417+
@Test
418+
void getParentShouldReturnParent() {
419+
ConfigurationPropertyName name = ConfigurationPropertyName.of("this.is.a.multipart.name");
420+
ConfigurationPropertyName p1 = name.getParent();
421+
ConfigurationPropertyName p2 = p1.getParent();
422+
ConfigurationPropertyName p3 = p2.getParent();
423+
ConfigurationPropertyName p4 = p3.getParent();
424+
ConfigurationPropertyName p5 = p4.getParent();
425+
assertThat(p1).hasToString("this.is.a.multipart");
426+
assertThat(p2).hasToString("this.is.a");
427+
assertThat(p3).hasToString("this.is");
428+
assertThat(p4).hasToString("this");
429+
assertThat(p5).isEqualTo(ConfigurationPropertyName.EMPTY);
430+
}
431+
432+
@Test
433+
void getParentWhenEmptyShouldReturnEmpty() {
434+
assertThat(ConfigurationPropertyName.EMPTY.getParent()).isEqualTo(ConfigurationPropertyName.EMPTY);
435+
}
436+
417437
@Test
418438
void chopWhenLessThenSizeShouldReturnChopped() {
419439
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo.bar.baz");
@@ -567,6 +587,15 @@ void equalsAndHashCode() {
567587
assertThat((Object) n14).isNotEqualTo(n15);
568588
}
569589

590+
@Test
591+
void equalsAndHashCodeAfterOperations() {
592+
ConfigurationPropertyName n1 = ConfigurationPropertyName.of("nested");
593+
ConfigurationPropertyName n2 = ConfigurationPropertyName.EMPTY.append("nested");
594+
ConfigurationPropertyName n3 = ConfigurationPropertyName.of("nested.value").getParent();
595+
assertThat(n1.hashCode()).isEqualTo(n2.hashCode()).isEqualTo(n3.hashCode());
596+
assertThat(n1).isEqualTo(n2).isEqualTo(n3);
597+
}
598+
570599
@Test
571600
void equalsWhenStartsWith() {
572601
// gh-14665

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertySourcesTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,21 @@ void environmentPropertyAccessWhenMutableShouldBeTolerable() {
143143
testPropertySourcePerformance(false, 5000);
144144
}
145145

146+
@Test // gh-21416
147+
void decendantOfPropertyAccessWhenMutableWithCacheShouldBePerformant() {
148+
StandardEnvironment environment = createPerformanceTestEnvironment(true);
149+
Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(environment);
150+
ConfigurationPropertyName missing = ConfigurationPropertyName.of("missing");
151+
long start = System.nanoTime();
152+
for (int i = 0; i < 1000; i++) {
153+
for (ConfigurationPropertySource source : sources) {
154+
assertThat(source.containsDescendantOf(missing)).isEqualTo(ConfigurationPropertyState.ABSENT);
155+
}
156+
}
157+
long total = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
158+
assertThat(total).isLessThan(1000);
159+
}
160+
146161
private void testPropertySourcePerformance(boolean immutable, int maxTime) {
147162
StandardEnvironment environment = createPerformanceTestEnvironment(immutable);
148163
testPropertySourcePerformance(environment, maxTime);

0 commit comments

Comments
 (0)