Skip to content

Commit 0427c3d

Browse files
nicklockwoodfacebook-github-bot-4
authored andcommitted
Added throttling on requests made by RCTImageLoader
Reviewed By: javache Differential Revision: D2938143 fb-gh-sync-id: bac1185d4792dcca0012905126c9ef2aa45905d5 shipit-source-id: bac1185d4792dcca0012905126c9ef2aa45905d5
1 parent 07a5f44 commit 0427c3d

File tree

6 files changed

+168
-33
lines changed

6 files changed

+168
-33
lines changed

Libraries/Image/RCTImageLoader.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ typedef void (^RCTImageLoaderCancellationBlock)(void);
2727

2828
@interface RCTImageLoader : NSObject <RCTBridgeModule, RCTURLRequestHandler>
2929

30+
/**
31+
* The maximum number of concurrent image loading tasks. Loading and decoding
32+
* images can consume a lot of memory, so setting this to a higher value may
33+
* cause memory to spike. If you are seeing out-of-memory crashes, try reducing
34+
* this value.
35+
*/
36+
@property (nonatomic, assign) NSUInteger maxConcurrentLoadingTasks;
37+
38+
/**
39+
* The maximum number of concurrent image decoding tasks. Decoding large
40+
* images can be especially CPU and memory intensive, so if your are decoding a
41+
* lot of large images in your app, you may wish to adjust this value.
42+
*/
43+
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingTasks;
44+
45+
/**
46+
* Decoding large images can use a lot of memory, and potentially cause the app
47+
* to crash. This value allows you to throttle the amount of memory used by the
48+
* decoder independently of the number of concurrent threads. This means you can
49+
* still decode a lot of small images in parallel, without allowing the decoder
50+
* to try to decompress multiple huge images at once. Note that this value is
51+
* only a hint, and not an indicator of the total memory used by the app.
52+
*/
53+
@property (nonatomic, assign) NSUInteger maxConcurrentDecodingBytes;
54+
3055
/**
3156
* Loads the specified image at the highest available resolution.
3257
* Can be called from any thread, will call back on an unspecified thread.

Libraries/Image/RCTImageLoader.m

Lines changed: 124 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ @implementation RCTImageLoader
4141
NSOperationQueue *_imageDecodeQueue;
4242
dispatch_queue_t _URLCacheQueue;
4343
NSURLCache *_URLCache;
44+
NSMutableArray *_pendingTasks;
45+
NSInteger _activeTasks;
46+
NSMutableArray *_pendingDecodes;
47+
NSInteger _scheduledDecodes;
48+
NSUInteger _activeBytes;
4449
}
4550

4651
@synthesize bridge = _bridge;
@@ -49,6 +54,11 @@ @implementation RCTImageLoader
4954

5055
- (void)setUp
5156
{
57+
// Set defaults
58+
_maxConcurrentLoadingTasks = _maxConcurrentLoadingTasks ?: 4;
59+
_maxConcurrentDecodingTasks = _maxConcurrentDecodingTasks ?: 2;
60+
_maxConcurrentDecodingBytes = _maxConcurrentDecodingBytes ?: 30 * 1024 *1024; // 30MB
61+
5262
// Get image loaders and decoders
5363
NSMutableArray<id<RCTImageURLLoader>> *loaders = [NSMutableArray array];
5464
NSMutableArray<id<RCTImageDataDecoder>> *decoders = [NSMutableArray array];
@@ -203,6 +213,50 @@ - (RCTImageLoaderCancellationBlock)loadImageWithTag:(NSString *)imageTag
203213
completionBlock:callback];
204214
}
205215

216+
- (void)dequeueTasks
217+
{
218+
dispatch_async(_URLCacheQueue, ^{
219+
220+
// Remove completed tasks
221+
for (RCTNetworkTask *task in _pendingTasks.reverseObjectEnumerator) {
222+
switch (task.status) {
223+
case RCTNetworkTaskFinished:
224+
[_pendingTasks removeObject:task];
225+
_activeTasks--;
226+
break;
227+
case RCTNetworkTaskPending:
228+
case RCTNetworkTaskInProgress:
229+
// Do nothing
230+
break;
231+
}
232+
}
233+
234+
// Start queued decode
235+
NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count;
236+
while (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
237+
activeDecodes <= _maxConcurrentDecodingTasks)) {
238+
dispatch_block_t decodeBlock = _pendingDecodes.firstObject;
239+
if (decodeBlock) {
240+
[_pendingDecodes removeObjectAtIndex:0];
241+
decodeBlock();
242+
} else {
243+
break;
244+
}
245+
}
246+
247+
// Start queued tasks
248+
for (RCTNetworkTask *task in _pendingTasks) {
249+
if (MAX(_activeTasks, _scheduledDecodes) >= _maxConcurrentLoadingTasks) {
250+
break;
251+
}
252+
if (task.status == RCTNetworkTaskPending) {
253+
[task start];
254+
_activeTasks++;
255+
}
256+
}
257+
});
258+
}
259+
206260
/**
207261
* This returns either an image, or raw image data, depending on the loading
208262
* path taken. This is useful if you want to skip decoding, e.g. when preloading
@@ -327,8 +381,7 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag
327381
}
328382

329383
// Download image
330-
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:
331-
^(NSURLResponse *response, NSData *data, NSError *error) {
384+
RCTNetworkTask *task = [_bridge.networking networkTaskWithRequest:request completionBlock:^(NSURLResponse *response, NSData *data, NSError *error) {
332385
if (error) {
333386
completionHandler(error, nil);
334387
return;
@@ -348,14 +401,26 @@ - (RCTImageLoaderCancellationBlock)loadImageOrDataWithTag:(NSString *)imageTag
348401
// Process image data
349402
processResponse(response, data, nil);
350403

404+
//clean up
405+
[weakSelf dequeueTasks];
406+
351407
});
352408

353409
}];
354410
task.downloadProgressBlock = progressHandler;
355-
[task start];
411+
412+
if (!_pendingTasks) {
413+
_pendingTasks = [NSMutableArray new];
414+
}
415+
[_pendingTasks addObject:task];
416+
if (MAX(_activeTasks, _scheduledDecodes) < _maxConcurrentLoadingTasks) {
417+
[task start];
418+
_activeTasks++;
419+
}
356420

357421
cancelLoad = ^{
358422
[task cancel];
423+
[weakSelf dequeueTasks];
359424
};
360425

361426
});
@@ -453,7 +518,6 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data
453518
__block volatile uint32_t cancelled = 0;
454519
void (^completionHandler)(NSError *, UIImage *) = ^(NSError *error, UIImage *image) {
455520
if ([NSThread isMainThread]) {
456-
457521
// Most loaders do not return on the main thread, so caller is probably not
458522
// expecting it, and may do expensive post-processing in the callback
459523
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@@ -475,40 +539,71 @@ - (RCTImageLoaderCancellationBlock)decodeImageDataWithoutClipping:(NSData *)data
475539
completionHandler:completionHandler];
476540
} else {
477541

478-
// Serialize decoding to prevent excessive memory usage
479-
if (!_imageDecodeQueue) {
480-
_imageDecodeQueue = [NSOperationQueue new];
481-
_imageDecodeQueue.name = @"com.facebook.react.ImageDecoderQueue";
482-
_imageDecodeQueue.maxConcurrentOperationCount = 2;
483-
}
484-
[_imageDecodeQueue addOperationWithBlock:^{
485-
if (cancelled) {
486-
return;
487-
}
542+
dispatch_async(_URLCacheQueue, ^{
543+
dispatch_block_t decodeBlock = ^{
544+
545+
// Calculate the size, in bytes, that the decompressed image will require
546+
NSInteger decodedImageBytes = (size.width * scale) * (size.height * scale) * 4;
488547

489-
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
548+
// Mark these bytes as in-use
549+
_activeBytes += decodedImageBytes;
550+
551+
// Do actual decompression on a concurrent background queue
552+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
553+
if (!cancelled) {
554+
555+
// Decompress the image data (this may be CPU and memory intensive)
556+
UIImage *image = RCTDecodeImageWithData(data, size, scale, resizeMode);
490557

491558
#if RCT_DEV
492559

493-
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
494-
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
495-
if (imagePixelSize.width * imagePixelSize.height >
496-
screenPixelSize.width * screenPixelSize.height) {
497-
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
498-
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
499-
NSStringFromCGSize(screenPixelSize));
500-
}
560+
CGSize imagePixelSize = RCTSizeInPixels(image.size, image.scale);
561+
CGSize screenPixelSize = RCTSizeInPixels(RCTScreenSize(), RCTScreenScale());
562+
if (imagePixelSize.width * imagePixelSize.height >
563+
screenPixelSize.width * screenPixelSize.height) {
564+
RCTLogInfo(@"[PERF ASSETS] Loading image at size %@, which is larger "
565+
"than the screen size %@", NSStringFromCGSize(imagePixelSize),
566+
NSStringFromCGSize(screenPixelSize));
567+
}
501568

502569
#endif
503570

504-
if (image) {
505-
completionHandler(nil, image);
571+
if (image) {
572+
completionHandler(nil, image);
573+
} else {
574+
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
575+
NSError *finalError = RCTErrorWithMessage(errorMessage);
576+
completionHandler(finalError, nil);
577+
}
578+
}
579+
580+
// We're no longer retaining the uncompressed data, so now we'll mark
581+
// the decoding as complete so that the loading task queue can resume.
582+
dispatch_async(_URLCacheQueue, ^{
583+
_scheduledDecodes--;
584+
_activeBytes -= decodedImageBytes;
585+
[self dequeueTasks];
586+
});
587+
});
588+
};
589+
590+
// The decode operation retains the compressed image data until it's
591+
// complete, so we'll mark it as having started, in order to block
592+
// further image loads from happening until we're done with the data.
593+
_scheduledDecodes++;
594+
595+
if (!_pendingDecodes) {
596+
_pendingDecodes = [NSMutableArray new];
597+
}
598+
NSInteger activeDecodes = _scheduledDecodes - _pendingDecodes.count - 1;
599+
if (activeDecodes == 0 || (_activeBytes <= _maxConcurrentDecodingBytes &&
600+
activeDecodes <= _maxConcurrentDecodingTasks)) {
601+
decodeBlock();
506602
} else {
507-
NSString *errorMessage = [NSString stringWithFormat:@"Error decoding image data <NSData %p; %tu bytes>", data, data.length];
508-
NSError *finalError = RCTErrorWithMessage(errorMessage);
509-
completionHandler(finalError, nil);
603+
[_pendingDecodes addObject:decodeBlock];
510604
}
511-
}];
605+
606+
});
512607

513608
return ^{
514609
OSAtomicOr32Barrier(1, &cancelled);

Libraries/Network/RCTNetworkTask.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ typedef void (^RCTURLRequestIncrementalDataBlock)(NSData *data);
1818
typedef void (^RCTURLRequestProgressBlock)(int64_t progress, int64_t total);
1919
typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
2020

21+
typedef NS_ENUM(NSInteger, RCTNetworkTaskStatus) {
22+
RCTNetworkTaskPending = 0,
23+
RCTNetworkTaskInProgress,
24+
RCTNetworkTaskFinished,
25+
};
26+
2127
@interface RCTNetworkTask : NSObject <RCTURLRequestDelegate>
2228

2329
@property (nonatomic, readonly) NSURLRequest *request;
@@ -31,6 +37,8 @@ typedef void (^RCTURLRequestResponseBlock)(NSURLResponse *response);
3137
@property (nonatomic, copy) RCTURLRequestResponseBlock responseBlock;
3238
@property (nonatomic, copy) RCTURLRequestProgressBlock uploadProgressBlock;
3339

40+
@property (nonatomic, readonly) RCTNetworkTaskStatus status;
41+
3442
- (instancetype)initWithRequest:(NSURLRequest *)request
3543
handler:(id<RCTURLRequestHandler>)handler
3644
completionBlock:(RCTURLRequestCompletionBlock)completionBlock NS_DESIGNATED_INITIALIZER;

Libraries/Network/RCTNetworkTask.m

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ - (instancetype)initWithRequest:(NSURLRequest *)request
3333
_request = request;
3434
_handler = handler;
3535
_completionBlock = completionBlock;
36+
_status = RCTNetworkTaskPending;
3637
}
3738
return self;
3839
}
@@ -55,6 +56,7 @@ - (void)start
5556
if ([self validateRequestToken:[_handler sendRequest:_request
5657
withDelegate:self]]) {
5758
_selfReference = self;
59+
_status = RCTNetworkTaskInProgress;
5860
}
5961
}
6062
}
@@ -66,6 +68,7 @@ - (void)cancel
6668
[_handler cancelRequest:strongToken];
6769
}
6870
[self invalidate];
71+
_status = RCTNetworkTaskFinished;
6972
}
7073

7174
- (BOOL)validateRequestToken:(id)requestToken
@@ -83,8 +86,9 @@ - (BOOL)validateRequestToken:(id)requestToken
8386
if (_completionBlock) {
8487
_completionBlock(_response, _data, [NSError errorWithDomain:RCTErrorDomain code:0
8588
userInfo:@{NSLocalizedDescriptionKey: @"Unrecognized request token."}]);
86-
[self invalidate];
8789
}
90+
[self invalidate];
91+
_status = RCTNetworkTaskFinished;
8892
return NO;
8993
}
9094
return YES;
@@ -130,8 +134,9 @@ - (void)URLRequest:(id)requestToken didCompleteWithError:(NSError *)error
130134
if ([self validateRequestToken:requestToken]) {
131135
if (_completionBlock) {
132136
_completionBlock(_response, _data, error);
133-
[self invalidate];
134137
}
138+
[self invalidate];
139+
_status = RCTNetworkTaskFinished;
135140
}
136141
}
137142

React/Profiler/RCTJSCProfiler.m

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ void RCTJSCProfilerStart(JSContextRef ctx)
9999
if (isProfiling) {
100100
NSString *filename = [NSString stringWithFormat:@"cpu_profile_%ld.json", (long)CFAbsoluteTimeGetCurrent()];
101101
outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent:filename];
102-
RCTNativeProfilerEnd(ctx, JSCProfileName, outputFile.UTF8String);
102+
if (RCTNativeProfilerEnd) {
103+
RCTNativeProfilerEnd(ctx, JSCProfileName, outputFile.UTF8String);
104+
}
103105
RCTLogInfo(@"Stopped JSC profiler for context: %p", ctx);
104106
} else {
105107
RCTLogWarn(@"Trying to stop JSC profiler on a context which is not being profiled.");

React/Profiler/RCTProfile.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ void RCTProfileInit(RCTBridge *bridge)
399399
dispatch_async(RCTProfileGetQueue(), ^{
400400
NSString *shadowQueue = @(dispatch_queue_get_label([[bridge uiManager] methodQueue]));
401401
NSArray *orderedThreads = @[@"JS async", RCTJSCThreadName, shadowQueue, @"main"];
402-
[orderedThreads enumerateObjectsUsingBlock:^(NSString *thread, NSUInteger idx, BOOL *stop) {
402+
[orderedThreads enumerateObjectsUsingBlock:^(NSString *thread, NSUInteger idx, __unused BOOL *stop) {
403403
RCTProfileAddEvent(RCTProfileTraceEvents,
404404
@"ph": @"M", // metadata event
405405
@"name": @"thread_sort_index",

0 commit comments

Comments
 (0)