diff --git a/build.gradle.kts b/build.gradle.kts index 7c2a0dcd..24cfb08b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,7 +20,7 @@ plugins { allprojects { group = "in.dragonbra" - version = "1.8.0" + version = "1.8.1-SNAPSHOT" } repositories { diff --git a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt index fa2d84e4..fa705a3a 100644 --- a/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt @@ -6,7 +6,7 @@ import `in`.dragonbra.javasteam.util.stream.BinaryReader import `in`.dragonbra.javasteam.util.stream.MemoryStream import `in`.dragonbra.javasteam.util.stream.SeekOrigin import org.tukaani.xz.LZMAInputStream -import java.util.zip.* +import java.util.zip.DataFormatException import kotlin.math.max @Suppress("SpellCheckingInspection", "unused") @@ -21,11 +21,6 @@ object VZipUtil { private const val VERSION: Byte = 'a'.code.toByte() - // Thread-local window buffer pool to avoid repeated allocations - private val windowBufferPool = ThreadLocal.withInitial { - ByteArray(1 shl 23) // 8MB max size - } - @JvmStatic fun decompress(ms: MemoryStream, destination: ByteArray, verifyChecksum: Boolean = true): Int { try { @@ -67,12 +62,7 @@ object VZipUtil { // If the value of dictionary size in properties is smaller than (1 << 12), // the LZMA decoder must set the dictionary size variable to (1 << 12). - val windowSize = max(1 shl 12, dictionarySize) - val windowBuffer = if (windowSize <= (1 shl 23)) { - windowBufferPool.get() // Reuse thread-local buffer - } else { - ByteArray(windowSize) // Fallback for unusually large windows - } + val windowBuffer = ByteArray(max(1 shl 12, dictionarySize)) val bytesRead = LZMAInputStream( ms, sizeDecompressed.toLong(), diff --git a/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt b/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt index ee3ea888..20dd000e 100644 --- a/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt +++ b/src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt @@ -1,7 +1,8 @@ package `in`.dragonbra.javasteam.util -import com.github.luben.zstd.Zstd +import com.github.luben.zstd.ZstdInputStream import `in`.dragonbra.javasteam.util.log.LogManager +import java.io.ByteArrayInputStream import java.io.IOException import java.nio.ByteBuffer import java.nio.ByteOrder @@ -9,6 +10,7 @@ import java.nio.ByteOrder object VZstdUtil { private const val VZSTD_HEADER: Int = 0x615A5356 + private const val STREAM_CHUNK_SIZE = 64 * 1024 // 64KB chunks private const val HEADER_SIZE = 8 private const val FOOTER_SIZE = 15 @@ -53,13 +55,31 @@ object VZstdUtil { throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.") } - val compressedData = buffer.copyOfRange(HEADER_SIZE, buffer.size - FOOTER_SIZE) // :( allocations - try { - val bytesDecompressed = Zstd.decompress(destination, compressedData) + // Use streaming decompression to avoid long JNI critical locks + var totalDecompressed = 0 + + // Use direct ByteArrayInputStream with offset to avoid copying compressed data + ByteArrayInputStream(buffer, HEADER_SIZE, buffer.size - FOOTER_SIZE).use { byteStream -> + ZstdInputStream(byteStream).use { zstdStream -> + var bytesRead: Int + var offset = 0 + + // Read in chunks to break up JNI locks + while (offset < sizeDecompressed) { + val toRead = minOf(STREAM_CHUNK_SIZE, sizeDecompressed - offset) + bytesRead = zstdStream.read(destination, offset, toRead) + + if (bytesRead == -1) break + + offset += bytesRead + totalDecompressed += bytesRead + } + } + } - if (bytesDecompressed != sizeDecompressed.toLong()) { - throw IOException("Failed to decompress Zstd (expected $sizeDecompressed bytes, got $bytesDecompressed).") + if (totalDecompressed != sizeDecompressed) { + throw IOException("Failed to decompress Zstd (expected $sizeDecompressed bytes, got $totalDecompressed).") } if (verifyChecksum) {