mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
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:
committed by
Greyson Parrelli
parent
c0f826808b
commit
1edc94d5ad
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user