From a0f0cf00b347297d2eff3f46c66f3f90b0c36e49 Mon Sep 17 00:00:00 2001 From: Junggi Kim Date: Thu, 10 Sep 2026 17:30:09 +0900 Subject: [PATCH] Avoid scanning the whole layer index for every entry IndexedLayers.getLayer() walked every entry of layers.idx for each jar entry being extracted, making extraction O(entries x index size). The dependency entries are listed first while the application classes are covered by a single directory entry near the end, so the most numerous entries consistently scanned the longest. Index the candidates once when the index file is read, keeping the directory candidates in a separate map that preserves index order. A file name is then resolved with a single map lookup and only the handful of directory candidates has to be scanned. For a jar with 24,823 entries and 700 dependencies this reduces the number of string comparisons from 17,179,582 to 72,946 (106.9ms to 2.5ms). With 4,173 entries it goes from 1,019,857 to 11,896 (5.4ms to 0.1ms) and with 903 entries from 70,612 to 2,426 (0.8ms to 0.1ms). Lookups resolve to the same layer as before. LayersIndex only writes a name once its whole subtree belongs to a single layer, so no indexed name is a prefix of another and at most one candidate can match, which makes the order the two kinds of candidate are consulted in immaterial. See gh-51653 Signed-off-by: Junggi Kim Co-Authored-By: Claude Opus 5 (1M context) --- .../boot/jarmode/tools/IndexedLayers.java | 22 ++++++++++++++----- .../jarmode/tools/IndexedLayersTests.java | 19 ++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java index 7f560d0656d..df2cc7eba84 100644 --- a/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java +++ b/loader/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java @@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -42,11 +43,16 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Madhura Bhave * @author Moritz Halbritter + * @author Junggi Kim */ class IndexedLayers implements Layers { private final Map> layers = new LinkedHashMap<>(); + private final Map layerByFileName = new HashMap<>(); + + private final Map layerByDirectoryName = new LinkedHashMap<>(); + private final String indexFileLocation; IndexedLayers(String indexFile, String indexFileLocation) { @@ -70,6 +76,10 @@ class IndexedLayers implements Layers { } } Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); + this.layers.forEach((layer, candidates) -> candidates.forEach((candidate) -> { + Map index = candidate.endsWith("/") ? this.layerByDirectoryName : this.layerByFileName; + index.putIfAbsent(candidate, layer); + })); } @Override @@ -84,11 +94,13 @@ class IndexedLayers implements Layers { @Override public String getLayer(String name) { - for (Map.Entry> entry : this.layers.entrySet()) { - for (String candidate : entry.getValue()) { - if (candidate.equals(name) || (candidate.endsWith("/") && name.startsWith(candidate))) { - return entry.getKey(); - } + String layer = this.layerByFileName.get(name); + if (layer != null) { + return layer; + } + for (Map.Entry entry : this.layerByDirectoryName.entrySet()) { + if (name.startsWith(entry.getKey())) { + return entry.getValue(); } } throw new IllegalStateException("No layer defined in index for file '" + name + "'"); diff --git a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java index 50053fb1a7b..f21d4444bbc 100644 --- a/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java +++ b/loader/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.mock; * * @author Phillip Webb * @author Madhura Bhave + * @author Junggi Kim */ class IndexedLayersTests { @@ -83,6 +84,13 @@ class IndexedLayersTests { assertThat(layers.getLayer(mockEntry("META-INF/a/sub/directory/and/a/file"))).isEqualTo("application"); } + @Test + void getLayerWhenNameIsInMultipleLayersReturnsFirstLayer() { + IndexedLayers layers = new IndexedLayers(createIndexWithDuplicateName(), "BOOT-INF/classes"); + assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("first"); + assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("first"); + } + @Test void getLayerWhenFileHasSpaceReturnsLayer() throws Exception { IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes"); @@ -102,6 +110,17 @@ class IndexedLayersTests { return getFile("test-layers.idx"); } + private String createIndexWithDuplicateName() { + return """ + - "first": + - "BOOT-INF/lib/a.jar" + - "META-INF/" + - "second": + - "BOOT-INF/lib/a.jar" + - "META-INF/" + """; + } + private String getFile(String fileName) throws Exception { ClassPathResource resource = new ClassPathResource(fileName, getClass()); InputStreamReader reader = new InputStreamReader(resource.getInputStream());