Skip to content

Commit 78e1225

Browse files
committed
Optimize VirtualZipDataBlock
Add some optimizations to `VirtualZipDataBlock` that help when sequentially reading the block from a JarInputStream. Closes gh-40125
1 parent 9b0593e commit 78e1225

File tree

3 files changed

+108
-11
lines changed

3 files changed

+108
-11
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@
1919
import java.io.IOException;
2020
import java.nio.ByteBuffer;
2121
import java.util.Collection;
22-
import java.util.List;
2322

2423
/**
2524
* A virtual {@link DataBlock} build from a collection of other {@link DataBlock}
@@ -29,10 +28,14 @@
2928
*/
3029
class VirtualDataBlock implements DataBlock {
3130

32-
private List<DataBlock> parts;
31+
private DataBlock[] parts;
32+
33+
private long[] offsets;
3334

3435
private long size;
3536

37+
private volatile int lastReadPart = 0;
38+
3639
/**
3740
* Create a new {@link VirtualDataBlock} instance. The {@link #setParts(Collection)}
3841
* method must be called before the data block can be used.
@@ -55,12 +58,16 @@ protected VirtualDataBlock() {
5558
* @throws IOException on I/O error
5659
*/
5760
protected void setParts(Collection<? extends DataBlock> parts) throws IOException {
58-
this.parts = List.copyOf(parts);
61+
this.parts = parts.toArray(DataBlock[]::new);
62+
this.offsets = new long[parts.size()];
5963
long size = 0;
64+
int i = 0;
6065
for (DataBlock part : parts) {
66+
this.offsets[i++] = size;
6167
size += part.size();
6268
}
6369
this.size = size;
70+
6471
}
6572

6673
@Override
@@ -73,20 +80,30 @@ public int read(ByteBuffer dst, long pos) throws IOException {
7380
if (pos < 0 || pos >= this.size) {
7481
return -1;
7582
}
83+
int lastReadPart = this.lastReadPart;
84+
int partIndex = 0;
7685
long offset = 0;
7786
int result = 0;
78-
for (DataBlock part : this.parts) {
87+
if (pos >= this.offsets[lastReadPart]) {
88+
partIndex = lastReadPart;
89+
offset = this.offsets[lastReadPart];
90+
}
91+
while (partIndex < this.parts.length) {
92+
DataBlock part = this.parts[partIndex];
7993
while (pos >= offset && pos < offset + part.size()) {
8094
int count = part.read(dst, pos - offset);
8195
result += Math.max(count, 0);
8296
if (count <= 0 || !dst.hasRemaining()) {
97+
this.lastReadPart = partIndex;
8398
return result;
8499
}
85100
pos += count;
86101
}
87102
offset += part.size();
103+
partIndex++;
88104
}
89105
return result;
106+
90107
}
91108

92109
}

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -76,12 +76,13 @@ private long addToCentral(List<DataBlock> parts, ZipCentralDirectoryFileHeaderRe
7676
.withOffsetToLocalHeader(offsetToLocalHeader);
7777
int originalExtraFieldLength = Short.toUnsignedInt(originalRecord.extraFieldLength());
7878
int originalFileCommentLength = Short.toUnsignedInt(originalRecord.fileCommentLength());
79-
DataBlock extraFieldAndComment = new DataPart(
80-
originalRecordPos + originalRecord.size() - originalExtraFieldLength - originalFileCommentLength,
81-
originalExtraFieldLength + originalFileCommentLength);
79+
int extraFieldAndCommentSize = originalExtraFieldLength + originalFileCommentLength;
8280
parts.add(new ByteArrayDataBlock(record.asByteArray()));
8381
parts.add(name);
84-
parts.add(extraFieldAndComment);
82+
if (extraFieldAndCommentSize > 0) {
83+
parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldAndCommentSize,
84+
extraFieldAndCommentSize));
85+
}
8586
return record.size();
8687
}
8788

@@ -93,7 +94,9 @@ private long addToLocal(List<DataBlock> parts, ZipCentralDirectoryFileHeaderReco
9394
int extraFieldLength = Short.toUnsignedInt(originalRecord.extraFieldLength());
9495
parts.add(new ByteArrayDataBlock(record.asByteArray()));
9596
parts.add(name);
96-
parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldLength, extraFieldLength));
97+
if (extraFieldLength > 0) {
98+
parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldLength, extraFieldLength));
99+
}
97100
parts.add(content);
98101
if (dataDescriptorRecord != null) {
99102
parts.add(new ByteArrayDataBlock(dataDescriptorRecord.asByteArray()));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.zip;
18+
19+
import java.io.File;
20+
import java.io.FileOutputStream;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.nio.file.Path;
24+
import java.util.Random;
25+
import java.util.concurrent.TimeUnit;
26+
import java.util.zip.ZipEntry;
27+
import java.util.zip.ZipInputStream;
28+
import java.util.zip.ZipOutputStream;
29+
30+
import org.junit.jupiter.api.Disabled;
31+
import org.junit.jupiter.api.Test;
32+
import org.junit.jupiter.api.io.TempDir;
33+
34+
/**
35+
* Performance tests for {@link ZipContent} that creates a {@link VirtualZipDataBlock}.
36+
*
37+
* @author Phillip Webb
38+
*/
39+
@Disabled("Only used for manual testing")
40+
public class VirtualZipPerformanceTests {
41+
42+
@TempDir
43+
Path temp;
44+
45+
@Test
46+
void sequentialReadPerformace() throws IOException {
47+
File file = createZipWithLargeEntries();
48+
long start = System.nanoTime();
49+
try (ZipContent zipContent = ZipContent.open(file.toPath(), "test/")) {
50+
try (InputStream in = zipContent.openRawZipData().asInputStream()) {
51+
ZipInputStream zip = new ZipInputStream(in);
52+
ZipEntry entry = zip.getNextEntry();
53+
while (entry != null) {
54+
entry = zip.getNextEntry();
55+
}
56+
}
57+
}
58+
System.out.println(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
59+
}
60+
61+
private File createZipWithLargeEntries() throws IOException {
62+
byte[] bytes = new byte[1024 * 1024];
63+
new Random().nextBytes(bytes);
64+
File file = this.temp.resolve("test.zip").toFile();
65+
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) {
66+
out.putNextEntry(new ZipEntry("test/"));
67+
out.closeEntry();
68+
for (int i = 0; i < 50; i++) {
69+
out.putNextEntry(new ZipEntry("test/" + i + ".dat"));
70+
out.write(bytes);
71+
out.closeEntry();
72+
}
73+
}
74+
return file;
75+
}
76+
77+
}

0 commit comments

Comments
 (0)