mirror of
https://github.com/immich-app/immich.git
synced 2026-01-23 09:58:56 -08:00
refactor, fix capacity handling
This commit is contained in:
@@ -30,20 +30,20 @@ class NativeByteBuffer(initialCapacity: Int) {
|
||||
var capacity = initialCapacity
|
||||
var offset = 0
|
||||
|
||||
fun ensureHeadroom(needed: Int = INITIAL_BUFFER_SIZE) {
|
||||
if (offset + needed > capacity) {
|
||||
capacity = (capacity * 2).coerceAtLeast(offset + needed)
|
||||
inline fun ensureHeadroom() {
|
||||
if (offset == capacity) {
|
||||
capacity *= 2
|
||||
pointer = NativeBuffer.realloc(pointer, capacity)
|
||||
}
|
||||
}
|
||||
|
||||
fun wrapRemaining() = NativeBuffer.wrap(pointer + offset, capacity - offset)
|
||||
inline fun wrapRemaining() = NativeBuffer.wrap(pointer + offset, capacity - offset)
|
||||
|
||||
fun advance(bytesRead: Int) {
|
||||
inline fun advance(bytesRead: Int) {
|
||||
offset += bytesRead
|
||||
}
|
||||
|
||||
fun free() {
|
||||
inline fun free() {
|
||||
if (pointer != 0L) {
|
||||
NativeBuffer.free(pointer)
|
||||
pointer = 0L
|
||||
|
||||
@@ -59,7 +59,7 @@ private open class LocalImagesPigeonCodec : StandardMessageCodec() {
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface LocalImageApi {
|
||||
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>>) -> Unit)
|
||||
fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||
fun cancelRequest(requestId: Long)
|
||||
fun getThumbhash(thumbhash: String, callback: (Result<Map<String, Long>>) -> Unit)
|
||||
|
||||
@@ -82,7 +82,7 @@ interface LocalImageApi {
|
||||
val widthArg = args[2] as Long
|
||||
val heightArg = args[3] as Long
|
||||
val isVideoArg = args[4] as Boolean
|
||||
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>> ->
|
||||
api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg) { result: Result<Map<String, Long>?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(LocalImagesPigeonUtils.wrapError(error))
|
||||
|
||||
@@ -27,7 +27,7 @@ import java.util.concurrent.Future
|
||||
data class Request(
|
||||
val taskFuture: Future<*>,
|
||||
val cancellationSignal: CancellationSignal,
|
||||
val callback: (Result<Map<String, Long>>) -> Unit
|
||||
val callback: (Result<Map<String, Long>?>) -> Unit
|
||||
)
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
@@ -71,7 +71,7 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
private val requestMap = ConcurrentHashMap<Long, Request>()
|
||||
|
||||
companion object {
|
||||
val CANCELLED = Result.success<Map<String, Long>>(mapOf())
|
||||
val CANCELLED = Result.success<Map<String, Long>?>(null)
|
||||
val OPTIONS = BitmapFactory.Options().apply { inPreferredConfig = Bitmap.Config.ARGB_8888 }
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
width: Long,
|
||||
height: Long,
|
||||
isVideo: Boolean,
|
||||
callback: (Result<Map<String, Long>>) -> Unit
|
||||
callback: (Result<Map<String, Long>?>) -> Unit
|
||||
) {
|
||||
val signal = CancellationSignal()
|
||||
val task = threadPool.submit {
|
||||
@@ -138,7 +138,7 @@ class LocalImagesImpl(context: Context) : LocalImageApi {
|
||||
width: Long,
|
||||
height: Long,
|
||||
isVideo: Boolean,
|
||||
callback: (Result<Map<String, Long>>) -> Unit,
|
||||
callback: (Result<Map<String, Long>?>) -> Unit,
|
||||
signal: CancellationSignal
|
||||
) {
|
||||
signal.throwIfCanceled()
|
||||
|
||||
@@ -47,7 +47,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface RemoteImageApi {
|
||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
|
||||
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>?>) -> Unit)
|
||||
fun cancelRequest(requestId: Long)
|
||||
|
||||
companion object {
|
||||
@@ -67,7 +67,7 @@ interface RemoteImageApi {
|
||||
val urlArg = args[0] as String
|
||||
val headersArg = args[1] as Map<String, String>
|
||||
val requestIdArg = args[2] as Long
|
||||
api.requestImage(urlArg, headersArg, requestIdArg) { result: Result<Map<String, Long>> ->
|
||||
api.requestImage(urlArg, headersArg, requestIdArg) { result: Result<Map<String, Long>?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(RemoteImagesPigeonUtils.wrapError(error))
|
||||
|
||||
@@ -46,14 +46,14 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
|
||||
val CANCELLED = Result.success<Map<String, Long>?>(null)
|
||||
}
|
||||
|
||||
override fun requestImage(
|
||||
url: String,
|
||||
headers: Map<String, String>,
|
||||
requestId: Long,
|
||||
callback: (Result<Map<String, Long>>) -> Unit
|
||||
callback: (Result<Map<String, Long>?>) -> Unit
|
||||
) {
|
||||
val signal = CancellationSignal()
|
||||
requestMap[requestId] = RemoteRequest(signal)
|
||||
@@ -167,7 +167,7 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
||||
.enableBrotli(true)
|
||||
.setStoragePath(storageDir.absolutePath)
|
||||
.setUserAgent(USER_AGENT)
|
||||
.enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, CACHE_SIZE_BYTES)
|
||||
// .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK, CACHE_SIZE_BYTES)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
||||
val requestBuilder = engine.newUrlRequestBuilder(url, callback, executor)
|
||||
headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) }
|
||||
val request = requestBuilder.build()
|
||||
signal.setOnCancelListener { request.cancel() }
|
||||
signal.setOnCancelListener(request::cancel)
|
||||
request.start()
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
||||
private val onFailure: (Exception) -> Unit,
|
||||
private val onComplete: () -> Unit,
|
||||
) : UrlRequest.Callback() {
|
||||
private var contentLength: Int = 0
|
||||
private var buffer: NativeByteBuffer? = null
|
||||
private var httpError: IOException? = null
|
||||
|
||||
@@ -235,9 +236,9 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
||||
return request.cancel()
|
||||
}
|
||||
|
||||
// Pre-size from Content-Length when available, otherwise use reasonable default
|
||||
val capacity = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull()
|
||||
?.takeIf { it > 0 } ?: INITIAL_BUFFER_SIZE
|
||||
contentLength = info.allHeaders["content-length"]?.firstOrNull()?.toIntOrNull() ?: 0
|
||||
// Cronet wants the buffer to always have free space, so increment by 1
|
||||
val capacity = if (contentLength > 0) contentLength + 1 else INITIAL_BUFFER_SIZE
|
||||
buffer = NativeByteBuffer(capacity)
|
||||
request.read(buffer!!.wrapRemaining())
|
||||
}
|
||||
@@ -248,7 +249,7 @@ private class CronetImageFetcher(context: Context, cacheDir: File) : ImageFetche
|
||||
byteBuffer: ByteBuffer
|
||||
) {
|
||||
buffer!!.apply {
|
||||
advance(byteBuffer.remaining())
|
||||
advance(byteBuffer.position())
|
||||
ensureHeadroom()
|
||||
}
|
||||
request.read(buffer!!.wrapRemaining())
|
||||
@@ -303,7 +304,7 @@ private class OkHttpImageFetcher private constructor(
|
||||
}
|
||||
.dispatcher(Dispatcher().apply { maxRequestsPerHost = MAX_REQUESTS_PER_HOST })
|
||||
.connectionPool(connectionPool)
|
||||
.cache(Cache(File(dir, "thumbnails"), CACHE_SIZE_BYTES))
|
||||
// .cache(Cache(File(dir, "thumbnails"), CACHE_SIZE_BYTES))
|
||||
|
||||
if (sslSocketFactory != null && trustManager != null) {
|
||||
builder.sslSocketFactory(sslSocketFactory, trustManager)
|
||||
@@ -345,8 +346,7 @@ private class OkHttpImageFetcher private constructor(
|
||||
) {
|
||||
synchronized(stateLock) {
|
||||
if (draining) {
|
||||
onFailure(IllegalStateException("Client is draining"))
|
||||
return
|
||||
return onFailure(IllegalStateException("Client is draining"))
|
||||
}
|
||||
activeCount++
|
||||
}
|
||||
@@ -354,7 +354,7 @@ private class OkHttpImageFetcher private constructor(
|
||||
val requestBuilder = Request.Builder().url(url)
|
||||
headers.forEach { (key, value) -> requestBuilder.addHeader(key, value) }
|
||||
val call = client.newCall(requestBuilder.build())
|
||||
signal.setOnCancelListener { call.cancel() }
|
||||
signal.setOnCancelListener(call::cancel)
|
||||
|
||||
call.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
@@ -363,45 +363,38 @@ private class OkHttpImageFetcher private constructor(
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
try {
|
||||
response.use {
|
||||
if (call.isCanceled()) {
|
||||
return onFailure(OperationCanceledException())
|
||||
}
|
||||
response.use {
|
||||
if (!response.isSuccessful) {
|
||||
return onFailure(IOException("HTTP ${response.code}: ${response.message}")).also { onComplete() }
|
||||
}
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
return onFailure(IOException("HTTP ${response.code}: ${response.message}"))
|
||||
}
|
||||
val body = response.body
|
||||
?: return onFailure(IOException("Empty response body")).also { onComplete() }
|
||||
|
||||
val body = response.body ?: return onFailure(IOException("Empty response body"))
|
||||
|
||||
val contentLength = body.contentLength()
|
||||
val capacity = if (contentLength > 0 && contentLength <= Int.MAX_VALUE) {
|
||||
contentLength.toInt()
|
||||
} else {
|
||||
INITIAL_BUFFER_SIZE
|
||||
}
|
||||
val buffer = NativeByteBuffer(capacity)
|
||||
if (call.isCanceled()) {
|
||||
onFailure(OperationCanceledException())
|
||||
return onComplete()
|
||||
}
|
||||
|
||||
val contentLength = body.contentLength().toInt()
|
||||
val capacity = if (contentLength > 0) contentLength + 1 else INITIAL_BUFFER_SIZE
|
||||
val buffer = NativeByteBuffer(capacity)
|
||||
body.source().use { source ->
|
||||
try {
|
||||
body.source().use { source ->
|
||||
while (true) {
|
||||
signal.throwIfCanceled()
|
||||
buffer.ensureHeadroom()
|
||||
val bytesRead = source.read(buffer.wrapRemaining())
|
||||
if (bytesRead == -1) break
|
||||
buffer.advance(bytesRead)
|
||||
}
|
||||
while (true) {
|
||||
if (call.isCanceled()) throw OperationCanceledException()
|
||||
val bytesRead = source.read(buffer.wrapRemaining())
|
||||
if (bytesRead == -1) break
|
||||
buffer.ensureHeadroom()
|
||||
buffer.advance(bytesRead)
|
||||
}
|
||||
|
||||
onSuccess(buffer)
|
||||
} catch (e: Exception) {
|
||||
buffer.free()
|
||||
onFailure(e)
|
||||
}
|
||||
onComplete()
|
||||
}
|
||||
} finally {
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -70,7 +70,7 @@ class LocalImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol LocalImageApi {
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||
func cancelRequest(requestId: Int64) throws
|
||||
func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
renderingIntent: .defaultIntent
|
||||
)!
|
||||
private static var requests = [Int64: LocalImageRequest]()
|
||||
private static let cancelledResult = Result<[String: Int64], any Error>.success([:])
|
||||
private static let cancelledResult = Result<[String: Int64]?, any Error>.success(nil)
|
||||
private static let concurrencySemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.activeProcessorCount * 2)
|
||||
private static let assetCache = {
|
||||
let assetCache = NSCache<NSString, PHAsset>()
|
||||
@@ -62,7 +62,7 @@ class LocalImageApiImpl: LocalImageApi {
|
||||
}
|
||||
}
|
||||
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64], any Error>) -> Void) {
|
||||
func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, completion: @escaping (Result<[String: Int64]?, any Error>) -> Void) {
|
||||
let request = LocalImageRequest(callback: completion)
|
||||
let item = DispatchWorkItem {
|
||||
if request.isCancelled {
|
||||
|
||||
@@ -70,7 +70,7 @@ class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol RemoteImageApi {
|
||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64], Error>) -> Void)
|
||||
func requestImage(url: String, headers: [String: String], requestId: Int64, completion: @escaping (Result<[String: Int64]?, Error>) -> Void)
|
||||
func cancelRequest(requestId: Int64) throws
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,16 @@ class LocalImageRequest extends ImageRequest {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, int> info = await localImageApi.requestImage(
|
||||
final info = await localImageApi.requestImage(
|
||||
localId,
|
||||
requestId: requestId,
|
||||
width: width,
|
||||
height: height,
|
||||
isVideo: assetType == AssetType.video,
|
||||
);
|
||||
if (info == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final frame = await _fromDecodedPlatformImage(info["pointer"]!, info["width"]!, info["height"]!, info["rowBytes"]!);
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
|
||||
@@ -12,8 +12,13 @@ class RemoteImageRequest extends ImageRequest {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, int> info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
|
||||
final frame = await _fromEncodedPlatformImage(info["pointer"]!, info["length"]!);
|
||||
final info = await remoteImageApi.requestImage(uri, headers: headers, requestId: requestId);
|
||||
final frame = switch (info) {
|
||||
{'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length),
|
||||
{'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} =>
|
||||
await _fromDecodedPlatformImage(pointer, width, height, rowBytes),
|
||||
_ => null,
|
||||
};
|
||||
return frame == null ? null : ImageInfo(image: frame.image, scale: scale);
|
||||
}
|
||||
|
||||
|
||||
9
mobile/lib/platform/local_image_api.g.dart
generated
9
mobile/lib/platform/local_image_api.g.dart
generated
@@ -49,7 +49,7 @@ class LocalImageApi {
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<Map<String, int>> requestImage(
|
||||
Future<Map<String, int>?> requestImage(
|
||||
String assetId, {
|
||||
required int requestId,
|
||||
required int width,
|
||||
@@ -79,13 +79,8 @@ class LocalImageApi {
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)!.cast<String, int>();
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)?.cast<String, int>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
mobile/lib/platform/remote_image_api.g.dart
generated
9
mobile/lib/platform/remote_image_api.g.dart
generated
@@ -49,7 +49,7 @@ class RemoteImageApi {
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<Map<String, int>> requestImage(
|
||||
Future<Map<String, int>?> requestImage(
|
||||
String url, {
|
||||
required Map<String, String> headers,
|
||||
required int requestId,
|
||||
@@ -71,13 +71,8 @@ class RemoteImageApi {
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)!.cast<String, int>();
|
||||
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)?.cast<String, int>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'package:pigeon/pigeon.dart';
|
||||
@HostApi()
|
||||
abstract class LocalImageApi {
|
||||
@async
|
||||
Map<String, int> requestImage(
|
||||
Map<String, int>? requestImage(
|
||||
String assetId, {
|
||||
required int requestId,
|
||||
required int width,
|
||||
|
||||
@@ -15,7 +15,7 @@ import 'package:pigeon/pigeon.dart';
|
||||
@HostApi()
|
||||
abstract class RemoteImageApi {
|
||||
@async
|
||||
Map<String, int> requestImage(
|
||||
Map<String, int>? requestImage(
|
||||
String url, {
|
||||
required Map<String, String> headers,
|
||||
required int requestId,
|
||||
|
||||
Reference in New Issue
Block a user