Improve memory usage of new APNG renderer by making it streaming.

This commit is contained in:
Greyson Parrelli
2026-03-20 21:50:12 -04:00
committed by Cody Henthorne
parent 48374e6950
commit 25b01a30be
13 changed files with 485 additions and 341 deletions

View File

@@ -33,7 +33,7 @@ import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.glide.cache.ApngDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ApngInputStreamFactoryResourceDecoder;
import org.thoughtcrime.securesms.glide.cache.ApngInputStreamResourceDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.ByteBufferApngDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngResourceEncoder;
@@ -93,10 +93,9 @@ public class SignalGlideComponents implements RegisterGlideComponents {
if (SignalStore.labs().getNewApngRenderer()) {
registry.prepend(InputStream.class, ApngDecoder.class, new ApngInputStreamResourceDecoder());
registry.prepend(InputStreamFactory.class, ApngDecoder.class, new ApngInputStreamFactoryResourceDecoder());
registry.prepend(ApngDecoder.class, new EncryptedApngResourceEncoder(secret));
registry.prepend(File.class, ApngDecoder.class, new EncryptedCacheDecoder<>(secret, new ApngInputStreamResourceDecoder()));
registry.prepend(File.class, ApngDecoder.class, new EncryptedApngCacheDecoder(secret));
registry.register(ApngDecoder.class, Drawable.class, new ApngDrawableTranscoder());
} else {
ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder();

View File

@@ -9,10 +9,8 @@ import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
import org.signal.core.util.readFully
import org.signal.glide.apng.ApngOptions
import org.signal.glide.common.io.InputStreamFactory
import java.io.ByteArrayInputStream
import java.io.IOException
class ApngInputStreamFactoryResourceDecoder : ResourceDecoder<InputStreamFactory, ApngDecoder> {
@@ -27,8 +25,7 @@ class ApngInputStreamFactoryResourceDecoder : ResourceDecoder<InputStreamFactory
@Throws(IOException::class)
override fun decode(source: InputStreamFactory, width: Int, height: Int, options: Options): Resource<ApngDecoder>? {
val data: ByteArray = source.create().readFully()
val decoder = ApngDecoder(ByteArrayInputStream(data))
return ApngResource(decoder, data.size)
val decoder = ApngDecoder.create { source.create() }
return ApngResource(decoder)
}
}

View File

@@ -1,39 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
import org.signal.core.util.readFully
import org.signal.core.util.stream.LimitedInputStream
import org.signal.glide.apng.ApngOptions
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
class ApngInputStreamResourceDecoder : ResourceDecoder<InputStream, ApngDecoder> {
companion object {
/** Set to match [com.bumptech.glide.load.data.InputStreamRewinder]'s read limit */
private const val READ_LIMIT: Long = 5 * 1024 * 1024
}
override fun handles(source: InputStream, options: Options): Boolean {
return if (options.get(ApngOptions.ANIMATE)!!) {
ApngDecoder.isApng(LimitedInputStream(source, READ_LIMIT))
} else {
false
}
}
@Throws(IOException::class)
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<ApngDecoder>? {
val data: ByteArray = source.readFully()
val decoder = ApngDecoder(ByteArrayInputStream(data))
return ApngResource(decoder, data.size)
}
}

View File

@@ -8,14 +8,14 @@ package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
class ApngResource(private val decoder: ApngDecoder, private val size: Int) : Resource<ApngDecoder> {
class ApngResource(private val decoder: ApngDecoder) : Resource<ApngDecoder> {
override fun getResourceClass(): Class<ApngDecoder> = ApngDecoder::class.java
override fun get(): ApngDecoder = decoder
override fun getSize(): Int = size
override fun getSize(): Int = 0
override fun recycle() {
decoder.inputStream.close()
decoder.close()
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
import org.signal.core.util.logging.Log
import org.signal.core.util.stream.LimitedInputStream
import org.signal.glide.apng.ApngOptions
import java.io.File
import java.io.IOException
internal class EncryptedApngCacheDecoder(private val secret: ByteArray) : EncryptedCoder(), ResourceDecoder<File, ApngDecoder> {
companion object {
private val TAG = Log.tag(EncryptedApngCacheDecoder::class.java)
private const val READ_LIMIT: Long = 5 * 1024 * 1024
}
override fun handles(source: File, options: Options): Boolean {
if (options.get(ApngOptions.ANIMATE) != true) {
return false
}
return try {
createEncryptedInputStream(secret, source).use { inputStream ->
ApngDecoder.isApng(LimitedInputStream(inputStream, READ_LIMIT))
}
} catch (e: IOException) {
Log.w(TAG, e)
false
}
}
@Throws(IOException::class)
override fun decode(source: File, width: Int, height: Int, options: Options): Resource<ApngDecoder>? {
val decoder = ApngDecoder.create { createEncryptedInputStream(secret, source) }
return ApngResource(decoder)
}
}

View File

@@ -17,11 +17,11 @@ internal class EncryptedApngResourceEncoder(private val secret: ByteArray) : Enc
override fun encode(data: Resource<ApngDecoder>, file: File, options: Options): Boolean {
try {
val input = data.get().inputStream
val input = data.get().streamFactory()
val output = createEncryptedOutputStream(secret, file)
input.reset()
input.copyTo(output)
input.close()
return true
} catch (e: IOException) {