From 78e12251e982b2df70d36d5da1e85ac4cbd30fdb Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 16 Apr 2024 20:18:22 -0700 Subject: [PATCH] Optimize VirtualZipDataBlock Add some optimizations to `VirtualZipDataBlock` that help when sequentially reading the block from a JarInputStream. Closes gh-40125 --- .../boot/loader/zip/VirtualDataBlock.java | 27 +++++-- .../boot/loader/zip/VirtualZipDataBlock.java | 15 ++-- .../zip/VirtualZipPerformanceTests.java | 77 +++++++++++++++++++ 3 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipPerformanceTests.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java index e8d8838700c..6fc1c5f20dd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualDataBlock.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ package org.springframework.boot.loader.zip; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Collection; -import java.util.List; /** * A virtual {@link DataBlock} build from a collection of other {@link DataBlock} @@ -29,10 +28,14 @@ import java.util.List; */ class VirtualDataBlock implements DataBlock { - private List parts; + private DataBlock[] parts; + + private long[] offsets; private long size; + private volatile int lastReadPart = 0; + /** * Create a new {@link VirtualDataBlock} instance. The {@link #setParts(Collection)} * method must be called before the data block can be used. @@ -55,12 +58,16 @@ class VirtualDataBlock implements DataBlock { * @throws IOException on I/O error */ protected void setParts(Collection parts) throws IOException { - this.parts = List.copyOf(parts); + this.parts = parts.toArray(DataBlock[]::new); + this.offsets = new long[parts.size()]; long size = 0; + int i = 0; for (DataBlock part : parts) { + this.offsets[i++] = size; size += part.size(); } this.size = size; + } @Override @@ -73,20 +80,30 @@ class VirtualDataBlock implements DataBlock { if (pos < 0 || pos >= this.size) { return -1; } + int lastReadPart = this.lastReadPart; + int partIndex = 0; long offset = 0; int result = 0; - for (DataBlock part : this.parts) { + if (pos >= this.offsets[lastReadPart]) { + partIndex = lastReadPart; + offset = this.offsets[lastReadPart]; + } + while (partIndex < this.parts.length) { + DataBlock part = this.parts[partIndex]; while (pos >= offset && pos < offset + part.size()) { int count = part.read(dst, pos - offset); result += Math.max(count, 0); if (count <= 0 || !dst.hasRemaining()) { + this.lastReadPart = partIndex; return result; } pos += count; } offset += part.size(); + partIndex++; } return result; + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java index 7b2541f4e07..69eda69b30c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/VirtualZipDataBlock.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,12 +76,13 @@ class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock .withOffsetToLocalHeader(offsetToLocalHeader); int originalExtraFieldLength = Short.toUnsignedInt(originalRecord.extraFieldLength()); int originalFileCommentLength = Short.toUnsignedInt(originalRecord.fileCommentLength()); - DataBlock extraFieldAndComment = new DataPart( - originalRecordPos + originalRecord.size() - originalExtraFieldLength - originalFileCommentLength, - originalExtraFieldLength + originalFileCommentLength); + int extraFieldAndCommentSize = originalExtraFieldLength + originalFileCommentLength; parts.add(new ByteArrayDataBlock(record.asByteArray())); parts.add(name); - parts.add(extraFieldAndComment); + if (extraFieldAndCommentSize > 0) { + parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldAndCommentSize, + extraFieldAndCommentSize)); + } return record.size(); } @@ -93,7 +94,9 @@ class VirtualZipDataBlock extends VirtualDataBlock implements CloseableDataBlock int extraFieldLength = Short.toUnsignedInt(originalRecord.extraFieldLength()); parts.add(new ByteArrayDataBlock(record.asByteArray())); parts.add(name); - parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldLength, extraFieldLength)); + if (extraFieldLength > 0) { + parts.add(new DataPart(originalRecordPos + originalRecord.size() - extraFieldLength, extraFieldLength)); + } parts.add(content); if (dataDescriptorRecord != null) { parts.add(new ByteArrayDataBlock(dataDescriptorRecord.asByteArray())); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipPerformanceTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipPerformanceTests.java new file mode 100644 index 00000000000..3e4e4ec4a99 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/VirtualZipPerformanceTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.zip; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Performance tests for {@link ZipContent} that creates a {@link VirtualZipDataBlock}. + * + * @author Phillip Webb + */ +@Disabled("Only used for manual testing") +public class VirtualZipPerformanceTests { + + @TempDir + Path temp; + + @Test + void sequentialReadPerformace() throws IOException { + File file = createZipWithLargeEntries(); + long start = System.nanoTime(); + try (ZipContent zipContent = ZipContent.open(file.toPath(), "test/")) { + try (InputStream in = zipContent.openRawZipData().asInputStream()) { + ZipInputStream zip = new ZipInputStream(in); + ZipEntry entry = zip.getNextEntry(); + while (entry != null) { + entry = zip.getNextEntry(); + } + } + } + System.out.println(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } + + private File createZipWithLargeEntries() throws IOException { + byte[] bytes = new byte[1024 * 1024]; + new Random().nextBytes(bytes); + File file = this.temp.resolve("test.zip").toFile(); + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(file))) { + out.putNextEntry(new ZipEntry("test/")); + out.closeEntry(); + for (int i = 0; i < 50; i++) { + out.putNextEntry(new ZipEntry("test/" + i + ".dat")); + out.write(bytes); + out.closeEntry(); + } + } + return file; + } + +}