Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ var slice = Array.prototype.slice;

var MethodTypes = keyMirror({
remote: null,
remoteAsync: null,
local: null,
});

type ErrorData = {
message: string;
domain: string;
code: number;
nativeStackIOS?: string;
};

/**
* Creates remotely invokable modules.
*/
Expand All @@ -36,21 +44,40 @@ var BatchedBridgeFactory = {
*/
_createBridgedModule: function(messageQueue, moduleConfig, moduleName) {
var remoteModule = mapObject(moduleConfig.methods, function(methodConfig, memberName) {
return methodConfig.type === MethodTypes.local ? null : function() {
var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null;
var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null;
var hasSuccCB = typeof lastArg === 'function';
var hasErrorCB = typeof secondLastArg === 'function';
hasErrorCB && invariant(
hasSuccCB,
'Cannot have a non-function arg after a function arg.'
);
var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0);
var args = slice.call(arguments, 0, arguments.length - numCBs);
var onSucc = hasSuccCB ? lastArg : null;
var onFail = hasErrorCB ? secondLastArg : null;
return messageQueue.call(moduleName, memberName, args, onFail, onSucc);
};
switch (methodConfig.type) {
case MethodTypes.remote:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be so happy when we get rid of this crazy block of code

return function() {
var lastArg = arguments.length > 0 ? arguments[arguments.length - 1] : null;
var secondLastArg = arguments.length > 1 ? arguments[arguments.length - 2] : null;
var hasErrorCB = typeof lastArg === 'function';
var hasSuccCB = typeof secondLastArg === 'function';
hasSuccCB && invariant(
hasErrorCB,
'Cannot have a non-function arg after a function arg.'
);
var numCBs = (hasSuccCB ? 1 : 0) + (hasErrorCB ? 1 : 0);
var args = slice.call(arguments, 0, arguments.length - numCBs);
var onSucc = hasSuccCB ? secondLastArg : null;
var onFail = hasErrorCB ? lastArg : null;
messageQueue.call(moduleName, memberName, args, onSucc, onFail);
};

case MethodTypes.remoteAsync:
return function(...args) {
return new Promise((resolve, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It bothers me that we use a real Promise here because it executes the calls in a setImmediate and therefore changes the ordering of the responses. Non promises will be executed first, then react batch processing, then finally the promises but outside of the batch.

Can we instead vend a super dumb promise polyfill that executes callbacks right away?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand what you're saying:

  1. Non-promise callbacks are invoked. Also the resolve/reject functions are invoked, which schedules setImmediate/nextTick.
  2. React batch processing -- this checks the message queue for new JS -> native calls and sends them to native(?)
  3. --- next tick ---
  4. The then/catch handlers of the promises run

I'm not sure what the right answer is though I have a couple of thoughts. Ideally this should not affect the program's correctness, since generally speaking it's not possible to know for sure when the promise will be fulfilled so better to program defensively. It may affect performance because of the extra tick and reduced batching -- wish that could be better. I do like the fact that global.Promise can be swapped out for a custom promise library like bluebird so features like long stack traces are available -- this is the main reason I would not be in favor of relying on a non-standard polyfill. Maybe the right tradeoff is to have a synchronous polyfill by default but still allow bluebird, etc. at the cost of the extra setImmediate tick.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your timeline is correct, and yes, it will not affect correctness but will affect performance and make debugging a tad harder as the queue won't be effectively executed in order.

The way batching works is that all the messages during a frame are sent via one call from obj-c to js. Before processing the first message, we start a flag saying that we are in batch mode, we process all the events and there's a lot of setState being fired, we call flush and React processes all the elements that have been dirtied.

I'm not suggesting to replace Promise globally, but just here returning a dumb object with a then and a catch method that executes synchronously.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think this is not a good idea, then we can just use Promise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking we would want the behavior and extra functions of global.Promise, like long stack traces and promise.done().

Setting out-of-order execution aside for a moment, it looks like the setImmediate handlers should be batched. That way we'll keep most of the performance though there will be two batches instead of one unless we refactor the code a bit.

Here's what I think we should do longer-term:

  1. Call ReactUpdates.batchedUpdate when processing the setImmediate handlers since they do not appear batched right now
  2. Rename setImmediate to process.nextTick. The current behavior of setImmediate is closer to process.nextTick than setImmediate since the handlers are synchronously invoked at the end of the JS event loop.
  3. Most Promise libraries will automatically use nextTick if it is available. Nothing should break.
  4. Implement setImmediate according to the spec or as much as is reasonable =)
  5. Convert most bridge methods to promises. Now they are batched and in order again (if everyone is using Promises).

I'd like to start with the built-in Promise library if that's OK, and then can optimize later if it's causing problems. The really sensitive code like the UIManager should keep using callbacks for now -- but modules like AsyncStorage and the network library are fine if they aren't batched since they fire at unpredictable times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a sample trace with bluebird's long stack traces enabled in JSC. Code looks like:

  componentDidMount() {
    new Promise((resolve) => resolve(1)).then(() => {
      require('NativeModules').Testing.testAsync().catch(
        (err) => console.log(err.stack)
      );
    });
  }
Error: Unknown error from a native module
    _createErrorFromErrorData@UIExplorer:7330:24
    _invokeCallback@UIExplorer:7659:15
    forEach@[native code]
    perform@UIExplorer:6188:24
    batchedUpdates@UIExplorer:18840:26
    batchedUpdates@UIExplorer:4720:34
    applyWithGuard@UIExplorer:882:25
    guardReturn@UIExplorer:7487:30
From previous event:
    callTimer@UIExplorer:7981:17
    callImmediates@UIExplorer:8030:34
    _flushedQueueUnguarded@UIExplorer:7855:37
    applyWithGuard@UIExplorer:882:25
    guardReturn@UIExplorer:7490:37
From previous event:
    UIExplorerApp_componentDidMount@UIExplorer:1157:12
    notifyAll@UIExplorer:4957:26
    close@UIExplorer:19254:35
    closeAll@UIExplorer:6261:29
    perform@UIExplorer:6202:24
    batchedMountComponentIntoNode@UIExplorer:20834:22
    batchedUpdates@UIExplorer:18838:15
    batchedUpdates@UIExplorer:4720:34
    renderComponent@UIExplorer:20903:32
    ReactMount__renderNewRootComponent@UIExplorer:5056:26
    render@UIExplorer:1350:39
    renderApplication@UIExplorer:40446:15
    run@UIExplorer:40387:34
    runApplication@UIExplorer:40409:26
    jsCall@UIExplorer:7438:34
    _callFunction@UIExplorer:7693:21
    forEach@[native code]
    perform@UIExplorer:6188:24
    batchedUpdates@UIExplorer:18840:26
    batchedUpdates@UIExplorer:4720:34
    applyWithGuard@UIExplorer:882:25
    guardReturn@UIExplorer:7487:30
    processBatch@UIExplorer:7712:23

It's not clean but it's really good that it shows the error originated from componentDidMount. Compare it to the error with the current Promise library and no long stack traces:

Error: Unknown error from a native module
    _createErrorFromErrorData@UIExplorer:7328:24
    UIExplorer:7283:54
    _invokeCallback@UIExplorer:7657:15
    UIExplorer:7720:39
    forEach@[native code]
    UIExplorer:7712:22
    perform@UIExplorer:6186:24
    batchedUpdates@UIExplorer:13850:26
    batchedUpdates@UIExplorer:4718:34
    UIExplorer:7711:34
    applyWithGuard@UIExplorer:882:25
    guardReturn@UIExplorer:7485:30
    processBatch@UIExplorer:7710:23
    [native code]
    global code

So I think this will help contribute to much better debugging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet :)

messageQueue.call(moduleName, memberName, args, resolve, (errorData) => {
var error = _createErrorFromErrorData(errorData);
reject(error);
});
});
};

case MethodTypes.local:
return null;

default:
throw new Error('Unknown bridge method type: ' + methodConfig.type);
}
});
for (var constName in moduleConfig.constants) {
warning(!remoteModule[constName], 'saw constant and method named %s', constName);
Expand All @@ -59,7 +86,6 @@ var BatchedBridgeFactory = {
return remoteModule;
},


create: function(MessageQueue, modulesConfig, localModulesConfig) {
var messageQueue = new MessageQueue(modulesConfig, localModulesConfig);
return {
Expand All @@ -80,4 +106,14 @@ var BatchedBridgeFactory = {
}
};

function _createErrorFromErrorData(errorData: ErrorData): Error {
var {
message,
...extraErrorInfo,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

destructuring like this is so awesome, I love it <3

} = errorData;
var error = new Error(message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: do we get a good stack trace when creating the error here? Is there a better place to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stack trace will have an extra frame for _createErrorFromErrorData but it's going to be full of internal methods with BatchedBridgeFactory and MessageQueue anyway. We could also use the Error.prepareStackTrace API (V8 only though) to modify the trace too.

The nice thing about this overall diff is that the Promise library can enable long stack traces. I have not tried it yet, but if a user wants to set global.Promise = bluebird and turn on debugging mode, I believe they will get long stack traces for free =)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do something like

error.framesToPop = 1;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a neat fb extension, will add this.

error.framesToPop = 1;
return Object.assign(error, extraErrorInfo);
}

module.exports = BatchedBridgeFactory;
14 changes: 7 additions & 7 deletions Libraries/Utilities/MessageQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,14 +431,14 @@ var MessageQueueMixin = {
},

/**
* @param {Function} onFail Function to store in current thread for later
* lookup, when request fails.
* @param {Function} onSucc Function to store in current thread for later
* lookup, when request succeeds.
* @param {Function} onFail Function to store in current thread for later
* lookup, when request fails.
* @param {Object?=} scope Scope to invoke `cb` with.
* @param {Object?=} res Resulting callback ids. Use `this._POOLED_CBIDS`.
*/
_storeCallbacksInCurrentThread: function(onFail, onSucc, scope) {
_storeCallbacksInCurrentThread: function(onSucc, onFail, scope) {
invariant(onFail || onSucc, INTERNAL_ERROR);
this._bookkeeping.allocateCallbackIDs(this._POOLED_CBIDS);
var succCBID = this._POOLED_CBIDS.successCallbackID;
Expand Down Expand Up @@ -494,18 +494,18 @@ var MessageQueueMixin = {
return ret;
},

call: function(moduleName, methodName, params, onFail, onSucc, scope) {
call: function(moduleName, methodName, params, onSucc, onFail, scope) {
invariant(
(!onFail || typeof onFail === 'function') &&
(!onSucc || typeof onSucc === 'function'),
'Callbacks must be functions'
);
// Store callback _before_ sending the request, just in case the MailBox
// returns the response in a blocking manner.
if (onSucc) {
this._storeCallbacksInCurrentThread(onFail, onSucc, scope, this._POOLED_CBIDS);
if (onSucc || onFail) {
this._storeCallbacksInCurrentThread(onSucc, onFail, scope, this._POOLED_CBIDS);
onSucc && params.push(this._POOLED_CBIDS.successCallbackID);
onFail && params.push(this._POOLED_CBIDS.errorCallbackID);
params.push(this._POOLED_CBIDS.successCallbackID);
}
var moduleID = this._remoteModuleNameToModuleID[moduleName];
if (moduleID === undefined || moduleID === null) {
Expand Down
86 changes: 83 additions & 3 deletions React/Base/RCTBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
RCTBridgeFieldFlushDateMillis
};

typedef NS_ENUM(NSUInteger, RCTJavaScriptFunctionKind) {
RCTJavaScriptFunctionKindNormal,
RCTJavaScriptFunctionKindAsync,
};

#ifdef __LP64__
typedef uint64_t RCTHeaderValue;
typedef struct section_64 RCTHeaderSection;
Expand Down Expand Up @@ -239,6 +244,7 @@ @interface RCTModuleMethod : NSObject
@property (nonatomic, copy, readonly) NSString *moduleClassName;
@property (nonatomic, copy, readonly) NSString *JSMethodName;
@property (nonatomic, assign, readonly) SEL selector;
@property (nonatomic, assign, readonly) RCTJavaScriptFunctionKind functionKind;

@end

Expand Down Expand Up @@ -420,6 +426,51 @@ - (instancetype)initWithReactMethodName:(NSString *)reactMethodName
} else if ([argumentName isEqualToString:@"RCTResponseSenderBlock"]) {
addBlockArgument();
useFallback = NO;
} else if ([argumentName isEqualToString:@"RCTPromiseResolveBlock"]) {
RCTAssert(i == numberOfArguments - 2,
@"The RCTPromiseResolveBlock must be the second to last parameter in -[%@ %@]",
_moduleClassName, objCMethodName);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise resolver ID", index,
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}

// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing RCTPromiseResolveBlock value = (^(id result) {
NSArray *arguments = result ? @[result] : @[];
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, arguments]
context:context];
});
)
useFallback = NO;
_functionKind = RCTJavaScriptFunctionKindAsync;
} else if ([argumentName isEqualToString:@"RCTPromiseRejectBlock"]) {
RCTAssert(i == numberOfArguments - 1,
@"The RCTPromiseRejectBlock must be the last parameter in -[%@ %@]",
_moduleClassName, objCMethodName);
RCT_ARG_BLOCK(
if (RCT_DEBUG && ![json isKindOfClass:[NSNumber class]]) {
RCTLogError(@"Argument %tu (%@) of %@.%@ must be a promise rejecter ID", index,
json, RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName);
return;
}

// Marked as autoreleasing, because NSInvocation doesn't retain arguments
__autoreleasing RCTPromiseRejectBlock value = (^(NSError *error) {
NSDictionary *errorData = [RCTModuleMethod dictionaryFromError:error
stackTrace:[NSThread callStackSymbols]];
[bridge _invokeAndProcessModule:@"BatchedBridge"
method:@"invokeCallbackAndReturnFlushedQueue"
arguments:@[json, @[errorData]]
context:context];
});
)
useFallback = NO;
_functionKind = RCTJavaScriptFunctionKindAsync;
}
}

Expand Down Expand Up @@ -498,9 +549,18 @@ - (void)invokeWithBridge:(RCTBridge *)bridge

// Safety check
if (arguments.count != _argumentBlocks.count) {
NSInteger actualCount = arguments.count;
NSInteger expectedCount = _argumentBlocks.count;

// Subtract the implicit Promise resolver and rejecter functions for implementations of async functions
if (_functionKind == RCTJavaScriptFunctionKindAsync) {
actualCount -= 2;
expectedCount -= 2;
}

RCTLogError(@"%@.%@ was called with %zd arguments, but expects %zd",
RCTBridgeModuleNameForClass(_moduleClass), _JSMethodName,
arguments.count, _argumentBlocks.count);
actualCount, expectedCount);
return;
}
}
Expand Down Expand Up @@ -528,6 +588,26 @@ - (NSString *)description
return [NSString stringWithFormat:@"<%@: %p; exports %@ as %@;>", NSStringFromClass(self.class), self, _methodName, _JSMethodName];
}

+ (NSDictionary *)dictionaryFromError:(NSError *)error stackTrace:(NSArray *)stackTrace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweet

{
NSString *errorMessage;
NSMutableDictionary *errorInfo = [NSMutableDictionary dictionaryWithDictionary:@{
@"nativeStackIOS": stackTrace,
}];

if (error) {
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
errorInfo[@"domain"] = error.domain ?: RCTErrorDomain;
errorInfo[@"code"] = @(error.code);
} else {
errorMessage = @"Unknown error from a native module";
errorInfo[@"domain"] = RCTErrorDomain;
errorInfo[@"code"] = @(-1);
}

return RCTMakeError(errorMessage, nil, errorInfo);
}

@end

/**
Expand Down Expand Up @@ -606,7 +686,7 @@ - (NSString *)description
* },
* "methodName2": {
* "methodID": 1,
* "type": "remote"
* "type": "remoteAsync"
* },
* etc...
* },
Expand All @@ -630,7 +710,7 @@ - (NSString *)description
[methods enumerateObjectsUsingBlock:^(RCTModuleMethod *method, NSUInteger methodID, BOOL *_stop) {
methodsByName[method.JSMethodName] = @{
@"methodID": @(methodID),
@"type": @"remote",
@"type": method.functionKind == RCTJavaScriptFunctionKindAsync ? @"remoteAsync" : @"remote",
};
}];

Expand Down
36 changes: 35 additions & 1 deletion React/Base/RCTBridgeModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@
*/
typedef void (^RCTResponseSenderBlock)(NSArray *response);

/**
* Block that bridge modules use to resolve the JS promise waiting for a result.
* Nil results are supported and are converted to JS's undefined value.
*/
typedef void (^RCTPromiseResolveBlock)(id result);

/**
* Block that bridge modules use to reject the JS promise waiting for a result.
* The error may be nil but it is preferable to pass an NSError object for more
* precise error messages.
*/
typedef void (^RCTPromiseRejectBlock)(NSError *error);


/**
* This constant can be returned from +methodQueue to force module
* methods to be called on the JavaScript thread. This can have serious
Expand All @@ -37,7 +51,7 @@ extern const dispatch_queue_t RCTJSThread;
* A reference to the RCTBridge. Useful for modules that require access
* to bridge features, such as sending events or making JS calls. This
* will be set automatically by the bridge when it initializes the module.
* To implement this in your module, just add @synthesize bridge = _bridge;
* To implement this in your module, just add @synthesize bridge = _bridge;
*/
@property (nonatomic, weak) RCTBridge *bridge;

Expand Down Expand Up @@ -70,6 +84,26 @@ extern const dispatch_queue_t RCTJSThread;
* { ... }
*
* and is exposed to JavaScript as `NativeModules.ModuleName.doSomething`.
*
* ## Promises
*
* Bridge modules can also define methods that are exported to JavaScript as
* methods that return promises and are compatible with JS async functions.
*
* Declare the last two parameters of your native method to be a resolver block
* and a rejecter block. The resolver block must precede the rejecter block.
*
* For example:
*
* RCT_EXPORT_METHOD(doSomethingAsync:(NSString *)aString
* resolver:(RCTPromiseResolveBlock)resolve
* rejecter:(RCTPromiseRejectBlock)reject
* { ... }
*
* Calling `NativeModules.ModuleName.doSomethingAsync(aString)` from
* JavaScript will return a promise that is resolved or rejected when your
* native method implementation calls the respective block.
*
*/
#define RCT_EXPORT_METHOD(method) \
RCT_REMAP_METHOD(, method)
Expand Down