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:
Nick Lockwood 2016-02-16 12:41:20 -08:00 committed by facebook-github-bot-4
parent 07a5f4407f
commit 0427c3d273
6 changed files with 169 additions and 34 deletions

View File

@ -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.

View File

@ -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);

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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.");

View File

@ -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",