mirror of
https://github.com/status-im/react-native.git
synced 2025-01-14 11:34:23 +00:00
Added throttling on requests made by RCTImageLoader
Reviewed By: javache Differential Revision: D2938143 fb-gh-sync-id: bac1185d4792dcca0012905126c9ef2aa45905d5 shipit-source-id: bac1185d4792dcca0012905126c9ef2aa45905d5
This commit is contained in:
parent
07a5f4407f
commit
0427c3d273
@ -27,6 +27,31 @@ typedef void (^RCTImageLoaderCancellationBlock)(void);
|
||||
|
||||
@interface RCTImageLoader : NSObject <RCTBridgeModule, RCTURLRequestHandler>
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent image loading tasks. Loading and decoding
|
||||
* images can consume a lot of memory, so setting this to a higher value may
|
||||
* cause memory to spike. If you are seeing out-of-memory crashes, try reducing
|
||||
* this value.
|
||||
*/
|
||||
@property (nonatomic, assign) NSUInteger maxConcurrentLoadingTasks;
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent image decoding tasks. Decoding large
|
||||
* images can be especially CPU and memory intensive, so if your are decoding a
|
||||
* lot of large images in your app, you may wish to adjust this value.
|
||||
*/
|
||||
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingTasks;
|
||||
|
||||
/**
|
||||
* Decoding large images can use a lot of memory, and potentially cause the app
|
||||
* to crash. This value allows you to throttle the amount of memory used by the
|
||||
* decoder independently of the number of concurrent threads. This means you can
|
||||
* still decode a lot of small images in parallel, without allowing the decoder
|
||||
* to try to decompress multiple huge images at once. Note that this value is
|
||||
* only a hint, and not an indicator of the total memory used by the app.
|
||||
*/
|
||||
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingBytes;
|
||||
|
||||
/**
|
||||
* Loads the specified image at the highest available resolution.
|
||||
* Can be called from any thread, will call back on an unspecified thread.
|
||||
|
@ -41,6 +41,11 @@
|
||||
NSOperationQueue *_imageDecodeQueue;
|
||||
dispatch_queue_t _URLCacheQueue;
|
||||
NSURLCache *_URLCache;
|
||||
NSMutableArray *_pendingTasks;
|
||||
NSInteger _activeTasks;
|
||||
NSMutableArray *_pendingDecodes;
|
||||
NSInteger _scheduledDecodes;
|
||||
NSUInteger _activeBytes;
|
||||
}
|
||||
|
||||
@synthesize bridge = _bridge;
|
||||
@ -49,6 +54,11 @@ RCT_EXPORT_MODULE()
|
||||
|
||||
- (void)setUp
|
||||
{
|
||||
// Set defaults
|
||||
_maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4;
|
||||
_maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2;
|
||||
_maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 *1024; // 30MB
|
||||
|
||||
// Get image loaders and decoders
|
||||
NSMutableArray<id<RCTImageURLLoader>> *loaders = [NSMutableArray array];
|
||||
NSMutableArray<id<RCTImageDataDecoder>> *decoders = [NSMutableArray array];
|
||||
@ -203,6 +213,50 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
||||
completionBlock:callback];
|
||||
}
|
||||
|
||||
- (void)dequeueTasks
|
||||
{
|
||||
dispatch_async(_URLCacheQueue, ^{
|
||||
|
||||
// Remove completed tasks
|
||||
for (RCTNetworkTask *task in _pendingTasks.reverseObjectEnumerator) {
|
||||
switch (task.status) {
|
||||
case RCTNetworkTaskFinished:
|
||||
[_pendingTasks removeObject:task];
|
||||
_activeTasks--;
|
||||
break;
|
||||
case RCTNetworkTaskPending:
|
||||
case RCTNetworkTaskInProgress:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start queued decode
|
||||
NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count;
|
||||
while (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
|
||||
activeDecodes <= _maxConcurrentDecodingTasks)) {
|
||||
dispatch_block_t decodeBlock = _pendingDecodes.firstObject;
|
||||
if (decodeBlock) {
|
||||
[_pendingDecodes removeObjectAtIndex:0];
|
||||
decodeBlock();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Start queued tasks
|
||||
for (RCTNetworkTask *task in _pendingTasks) {
|
||||
if (MAX(_activeTasks, _scheduledDecodes) >= _maxConcurrentLoadingTasks) {
|
||||
break;
|
||||
}
|
||||
if (task.status == RCTNetworkTaskPending) {
|
||||
[task start];
|
||||
_activeTasks++;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This returns either an image, or raw image data, depending on the loading
|
||||
* path taken. This is useful if you want to skip decoding, e.g. when preloading
|
||||
@ -327,8 +381,7 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
||||
}
|
||||
|
||||
// Download image
|
||||
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
|
||||
^(NSURLResponse *response, NSData *data, NSError *error) {
|
||||
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
|
||||
if (error) {
|
||||
completionHandler(error, nil);
|
||||
return;
|
||||
@ -348,14 +401,26 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
||||
// Process image data
|
||||
processResponse(response, data, nil);
|
||||
|
||||
//clean up
|
||||
[weakSelf dequeueTasks];
|
||||
|
||||
});
|
||||
|
||||
}];
|
||||
task.downloadProgressBlock = progressHandler;
|
||||
[task start];
|
||||
|
||||
if (!_pendingTasks) {
|
||||
_pendingTasks = [NSMutableArray new];
|
||||
}
|
||||
[_pendingTasks addObject:task];
|
||||
if (MAX(_activeTasks, _scheduledDecodes) < _maxConcurrentLoadingTasks) {
|
||||
[task start];
|
||||
_activeTasks++;
|
||||
}
|
||||
|
||||
cancelLoad = ^{
|
||||
[task cancel];
|
||||
[weakSelf dequeueTasks];
|
||||
};
|
||||
|
||||
});
|
||||
@ -453,7 +518,6 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
||||
__block volatile uint32_t cancelled = 0;
|
||||
void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) {
|
||||
if ([NSThread isMainThread]) {
|
||||
|
||||
// Most loaders do not return on the main thread, so caller is probably not
|
||||
// expecting it, and may do expensive post-processing in the callback
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
@ -475,40 +539,71 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image,
|
||||
completionHandler:completionHandler];
|
||||
} else {
|
||||
|
||||
// Serialize decoding to prevent excessive memory usage
|
||||
if (!_imageDecodeQueue) {
|
||||
_imageDecodeQueue = [NSOperationQueue new];
|
||||
_imageDecodeQueue.name = @"com.facebook.react.ImageDecoderQueue";
|
||||
_imageDecodeQueue.maxConcurrentOperationCount = 2;
|
||||
}
|
||||
[_imageDecodeQueue addOperationWithBlock:^{
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
dispatch_async(_URLCacheQueue, ^{
|
||||
dispatch_block_t decodeBlock = ^{
|
||||
|
||||
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
|
||||
// Calculate the size, in bytes, that the decompressed image will require
|
||||
NSInteger decodedImageBytes = (size.width * scale) * (size.height * scale) * 4;
|
||||
|
||||
// Mark these bytes as in-use
|
||||
_activeBytes += decodedImageBytes;
|
||||
|
||||
// Do actual decompression on a concurrent background queue
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if (!cancelled) {
|
||||
|
||||
// Decompress the image data (this may be CPU and memory intensive)
|
||||
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
|
||||
|
||||
#if RCT_DEV
|
||||
|
||||
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
|
||||
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
|
||||
if (imagePixelSize.width * imagePixelSize.height >
|
||||
screenPixelSize.width * screenPixelSize.height) {
|
||||
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
|
||||
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
|
||||
NSStringFromCGSize(screenPixelSize));
|
||||
}
|
||||
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
|
||||
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
|
||||
if (imagePixelSize.width * imagePixelSize.height >
|
||||
screenPixelSize.width * screenPixelSize.height) {
|
||||
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
|
||||
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
|
||||
NSStringFromCGSize(screenPixelSize));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
if (image) {
|
||||
completionHandler(nil, image);
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
|
||||
NSError *finalError = RCTErrorWithMessage(errorMessage);
|
||||
completionHandler(finalError, nil);
|
||||
if (image) {
|
||||
completionHandler(nil, image);
|
||||
} else {
|
||||
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
|
||||
NSError *finalError = RCTErrorWithMessage(errorMessage);
|
||||
completionHandler(finalError, nil);
|
||||
}
|
||||
}
|
||||
|
||||
// We're no longer retaining the uncompressed data, so now we'll mark
|
||||
// the decoding as complete so that the loading task queue can resume.
|
||||
dispatch_async(_URLCacheQueue, ^{
|
||||
_scheduledDecodes--;
|
||||
_activeBytes -= decodedImageBytes;
|
||||
[self dequeueTasks];
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// The decode operation retains the compressed image data until it's
|
||||
// complete, so we'll mark it as having started, in order to block
|
||||
// further image loads from happening until we're done with the data.
|
||||
_scheduledDecodes++;
|
||||
|
||||
if (!_pendingDecodes) {
|
||||
_pendingDecodes = [NSMutableArray new];
|
||||
}
|
||||
}];
|
||||
NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count - 1;
|
||||
if (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
|
||||
activeDecodes <= _maxConcurrentDecodingTasks)) {
|
||||
decodeBlock();
|
||||
} else {
|
||||
[_pendingDecodes addObject:decodeBlock];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return ^{
|
||||
OSAtomicOr32Barrier(1, &cancelled);
|
||||
|
@ -18,6 +18,12 @@ typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
|
||||
typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total);
|
||||
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
|
||||
|
||||
typedef NS_ENUM(NSInteger, RCTNetworkTaskStatus) {
|
||||
RCTNetworkTaskPending = 0,
|
||||
RCTNetworkTaskInProgress,
|
||||
RCTNetworkTaskFinished,
|
||||
};
|
||||
|
||||
@interface RCTNetworkTask : NSObject <RCTURLRequestDelegate>
|
||||
|
||||
@property (nonatomic, readonly) NSURLRequest *request;
|
||||
@ -31,6 +37,8 @@ typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
|
||||
@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock;
|
||||
@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock;
|
||||
|
||||
@property (nonatomic, readonly) RCTNetworkTaskStatus status;
|
||||
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request
|
||||
handler:(id<RCTURLRequestHandler>)handler
|
||||
completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER;
|
||||
|
@ -33,6 +33,7 @@
|
||||
_request = request;
|
||||
_handler = handler;
|
||||
_completionBlock = completionBlock;
|
||||
_status = RCTNetworkTaskPending;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@ -55,6 +56,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||
if ([self validateRequestToken:[_handler sendRequest:_request
|
||||
withDelegate:self]]) {
|
||||
_selfReference = self;
|
||||
_status = RCTNetworkTaskInProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -66,6 +68,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||
[_handler cancelRequest:strongToken];
|
||||
}
|
||||
[self invalidate];
|
||||
_status = RCTNetworkTaskFinished;
|
||||
}
|
||||
|
||||
- (BOOL)validateRequestToken:(id)requestToken
|
||||
@ -83,8 +86,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||
if (_completionBlock) {
|
||||
_completionBlock(_response, _data, [NSError errorWithDomain:RCTErrorDomain code:0
|
||||
userInfo:@{NSLocalizedDescriptionKey: @"Unrecognized request token."}]);
|
||||
[self invalidate];
|
||||
}
|
||||
[self invalidate];
|
||||
_status = RCTNetworkTaskFinished;
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
@ -130,8 +134,9 @@ RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
||||
if ([self validateRequestToken:requestToken]) {
|
||||
if (_completionBlock) {
|
||||
_completionBlock(_response, _data, error);
|
||||
[self invalidate];
|
||||
}
|
||||
[self invalidate];
|
||||
_status = RCTNetworkTaskFinished;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,9 @@ NSString *RCTJSCProfilerStop(JSContextRef ctx)
|
||||
if (isProfiling) {
|
||||
NSString *filename = [NSString stringWithFormat:@"cpu_profile_%ld.json", (long)CFAbsoluteTimeGetCurrent()];
|
||||
outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
|
||||
RCTNativeProfilerEnd(ctx, JSCProfileName, outputFile.UTF8String);
|
||||
if (RCTNativeProfilerEnd) {
|
||||
RCTNativeProfilerEnd(ctx, JSCProfileName, outputFile.UTF8String);
|
||||
}
|
||||
RCTLogInfo(@"Stopped JSC profiler for context: %p", ctx);
|
||||
} else {
|
||||
RCTLogWarn(@"Trying to stop JSC profiler on a context which is not being profiled.");
|
||||
|
@ -399,7 +399,7 @@ void RCTProfileInit(RCTBridge *bridge)
|
||||
dispatch_async(RCTProfileGetQueue(), ^{
|
||||
NSString *shadowQueue = @(dispatch_queue_get_label([[bridge uiManager] methodQueue]));
|
||||
NSArray *orderedThreads = @[@"JS async", RCTJSCThreadName, shadowQueue, @"main"];
|
||||
[orderedThreads enumerateObjectsUsingBlock:^(NSString *thread, NSUInteger idx, BOOL *stop) {
|
||||
[orderedThreads enumerateObjectsUsingBlock:^(NSString *thread, NSUInteger idx, __unused BOOL *stop) {
|
||||
RCTProfileAddEvent(RCTProfileTraceEvents,
|
||||
@"ph": @"M", // metadata event
|
||||
@"name": @"thread_sort_index",
|
||||
|
Loading…
x
Reference in New Issue
Block a user