Dynamically calculate Glide stream mark limit based on device memory.

Replaces the hardcoded `InputStreamRewinder.MARK_READ_LIMIT` with a value that is calculated dynamically based on the device's RAM (scaling the mark read limit linearly between 5 to 8 MB).

A larger limit will avoid unnecessary fail & retries on high-end devices and improve performance.
This commit is contained in:
jeffrey-signal
2025-08-08 14:16:26 -04:00
committed by Greyson Parrelli
parent c0f826808b
commit 1edc94d5ad
6 changed files with 239 additions and 38 deletions

View File

@@ -1,6 +1,19 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* 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 http://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.thoughtcrime.securesms.glide;

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide
import android.app.ActivityManager
import android.content.Context
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
import org.signal.core.util.gibiBytes
import org.signal.core.util.mebiBytes
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.glide.GlideStreamConfig.MAX_MARK_LIMIT
import org.thoughtcrime.securesms.glide.GlideStreamConfig.MIN_MARK_LIMIT
object GlideStreamConfig {
private val MIN_MARK_LIMIT: ByteSize = 5.mebiBytes // Glide default
private val MAX_MARK_LIMIT: ByteSize = 8.mebiBytes
private val LOW_MEMORY_THRESHOLD: ByteSize = 4.gibiBytes
private val HIGH_MEMORY_THRESHOLD: ByteSize = 12.gibiBytes
@JvmStatic
val markReadLimitBytes: Int by lazy { calculateScaledMarkLimit(context = AppDependencies.application).inWholeBytes.toInt() }
/**
* Calculates buffer size, scaling proportionally from [MIN_MARK_LIMIT] to [MAX_MARK_LIMIT] based on how much memory the device has.
*/
private fun calculateScaledMarkLimit(context: Context): ByteSize {
val deviceMemory = getAvailableDeviceMemory(context)
return when {
deviceMemory <= LOW_MEMORY_THRESHOLD -> MIN_MARK_LIMIT
deviceMemory >= HIGH_MEMORY_THRESHOLD -> MAX_MARK_LIMIT
else -> calculateScaledSize(deviceMemory)
}
}
private fun getAvailableDeviceMemory(context: Context): ByteSize {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo().apply {
activityManager.getMemoryInfo(this)
}
return memoryInfo.totalMem.bytes
}
private fun calculateScaledSize(deviceMemory: ByteSize): ByteSize {
val ratio: Float = (deviceMemory - LOW_MEMORY_THRESHOLD).percentageOf(HIGH_MEMORY_THRESHOLD - LOW_MEMORY_THRESHOLD)
val offsetBytes = (ratio * (MAX_MARK_LIMIT.inWholeBytes - MIN_MARK_LIMIT.inWholeBytes)).toLong()
return MIN_MARK_LIMIT + ByteSize(offsetBytes)
}
}

View File

@@ -1,6 +1,19 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* 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 http://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.thoughtcrime.securesms.glide;

View File

@@ -9,6 +9,7 @@ import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream
import org.thoughtcrime.securesms.mms.InputStreamFactory
import org.thoughtcrime.securesms.util.BitmapUtil
import java.io.IOException
@@ -25,10 +26,31 @@ object ImageHeaderParserUtils {
@Throws(IOException::class)
fun getType(
parsers: List<ImageHeaderParser>,
inputStream: InputStream,
inputStream: InputStream?,
byteArrayPool: ArrayPool
): ImageHeaderParser.ImageType {
return GlideImageHeaderParserUtils.getType(parsers, inputStream, byteArrayPool)
if (inputStream == null) {
return ImageHeaderParser.ImageType.UNKNOWN
}
val markableStream = if (!inputStream.markSupported()) {
RecyclableBufferedInputStream(inputStream, byteArrayPool)
} else {
inputStream
}
markableStream.mark(GlideStreamConfig.markReadLimitBytes)
return getType(
parsers = parsers,
getTypeAndRewind = { parser ->
try {
parser.getType(markableStream)
} finally {
markableStream.reset()
}
}
)
}
/**
@@ -56,6 +78,16 @@ object ImageHeaderParserUtils {
return GlideImageHeaderParserUtils.getType(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
private fun getType(
parsers: List<ImageHeaderParser>,
getTypeAndRewind: (ImageHeaderParser) -> ImageHeaderParser.ImageType
): ImageHeaderParser.ImageType {
return parsers.firstNotNullOfOrNull { parser ->
getTypeAndRewind(parser)
.takeIf { type -> type != ImageHeaderParser.ImageType.UNKNOWN }
} ?: ImageHeaderParser.ImageType.UNKNOWN
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@@ -82,14 +114,43 @@ object ImageHeaderParserUtils {
return GlideImageHeaderParserUtils.getOrientation(parsers, parcelFileDescriptorRewinder, byteArrayPool)
}
/**
* @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation
*/
@JvmStatic
@Throws(IOException::class)
fun getOrientation(
parsers: List<ImageHeaderParser>,
inputStream: InputStream,
byteArrayPool: ArrayPool
inputStream: InputStream?,
byteArrayPool: ArrayPool,
allowStreamRewind: Boolean
): Int {
return GlideImageHeaderParserUtils.getOrientation(parsers, inputStream, byteArrayPool)
if (inputStream == null) {
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
val markableStream = if (allowStreamRewind && !inputStream.markSupported()) {
RecyclableBufferedInputStream(inputStream, byteArrayPool)
} else {
inputStream
}
if (allowStreamRewind) {
markableStream.mark(GlideStreamConfig.markReadLimitBytes)
}
return getOrientation(
parsers = parsers,
getOrientationAndRewind = { parser ->
try {
parser.getOrientation(markableStream, byteArrayPool)
} finally {
if (allowStreamRewind) {
markableStream.reset()
}
}
}
)
}
/**
@@ -102,49 +163,27 @@ object ImageHeaderParserUtils {
inputStreamFactory: InputStreamFactory,
byteArrayPool: ArrayPool
): Int {
val orientationFromParsers = getOrientationFromParsers(
val orientationFromParsers = getOrientation(
parsers = parsers,
inputStream = inputStreamFactory.createRecyclable(byteArrayPool),
byteArrayPool = byteArrayPool
byteArrayPool = byteArrayPool,
allowStreamRewind = false
)
if (orientationFromParsers != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromParsers
val orientationFromExif = getOrientationFromExif(inputStream = inputStreamFactory.createRecyclable(byteArrayPool))
val orientationFromExif = BitmapUtil.getExifOrientation(ExifInterface(inputStreamFactory.createRecyclable(byteArrayPool)))
if (orientationFromExif != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromExif
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
private fun getOrientationFromParsers(
parsers: List<ImageHeaderParser>,
inputStream: InputStream?,
byteArrayPool: ArrayPool
): Int {
if (inputStream == null) {
return ImageHeaderParser.UNKNOWN_ORIENTATION
}
return getOrientation(
parsers = parsers,
readOrientation = { parser -> parser.getOrientation(inputStream, byteArrayPool) }
)
}
private fun getOrientationFromExif(inputStream: InputStream): Int {
return BitmapUtil.getExifOrientation(ExifInterface(inputStream))
}
private fun getOrientation(
parsers: List<ImageHeaderParser>,
readOrientation: (ImageHeaderParser) -> Int
getOrientationAndRewind: (ImageHeaderParser) -> Int
): Int {
parsers.forEach { parser ->
val orientation = readOrientation(parser)
if (orientation != ImageHeaderParser.UNKNOWN_ORIENTATION) {
return orientation
}
}
return ImageHeaderParser.UNKNOWN_ORIENTATION
return parsers.firstNotNullOfOrNull { parser ->
getOrientationAndRewind(parser)
.takeIf { type -> type != ImageHeaderParser.UNKNOWN_ORIENTATION }
} ?: ImageHeaderParser.UNKNOWN_ORIENTATION
}
}

View File

@@ -18,7 +18,6 @@ import androidx.annotation.RequiresApi;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ImageHeaderParser.ImageType;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.data.InputStreamRewinder;
import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
@@ -209,7 +208,7 @@ interface ImageReader {
@Override
public int getImageOrientation() throws IOException {
try {
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder.rewindAndGet(), byteArrayPool);
return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder.rewindAndGet(), byteArrayPool, true);
} catch (IOException e) {
return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, inputStreamFactory, byteArrayPool);
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*
* This file is adapted from the Glide image loading library.
* Original work Copyright 2014 Google Inc. All rights reserved.
*
* 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 http://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.thoughtcrime.securesms.glide;
import androidx.annotation.NonNull;
import com.bumptech.glide.load.data.DataRewinder;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream;
import com.bumptech.glide.util.Synthetic;
import java.io.IOException;
import java.io.InputStream;
/**
* Implementation for {@link InputStream}s that rewinds streams by wrapping them in a buffered stream. This is a copy of
* {@link com.bumptech.glide.load.data.InputStreamRewinder that is modified to use GlideStreamConfig.markReadLimitBytes in place of a hardcoded MARK_READ_LIMIT.
*/
public final class InputStreamRewinder implements DataRewinder<InputStream> {
private final RecyclableBufferedInputStream bufferedStream;
@Synthetic
public InputStreamRewinder(InputStream is, ArrayPool byteArrayPool) {
// We don't check is.markSupported() here because RecyclableBufferedInputStream allows resetting
// after exceeding GlideStreamConfig.markReadLimitBytes, which other InputStreams don't guarantee.
bufferedStream = new RecyclableBufferedInputStream(is, byteArrayPool);
bufferedStream.mark(GlideStreamConfig.getMarkReadLimitBytes());
}
@NonNull
@Override
public InputStream rewindAndGet() throws IOException {
bufferedStream.reset();
return bufferedStream;
}
@Override
public void cleanup() {
bufferedStream.release();
}
public void fixMarkLimits() {
bufferedStream.fixMarkLimit();
}
/**
* Factory for producing {@link InputStreamRewinder}s from {@link InputStream}s.
*/
public static final class Factory implements DataRewinder.Factory<InputStream> {
private final ArrayPool byteArrayPool;
public Factory(ArrayPool byteArrayPool) {
this.byteArrayPool = byteArrayPool;
}
@NonNull
@Override
public DataRewinder<InputStream> build(@NonNull InputStream data) {
return new InputStreamRewinder(data, byteArrayPool);
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
}
}