Repository navigation
Convert iOS native module objects to JS with fewer lookups and retains - #59450
hryhoriiK97 wants to merge 6 commits into
Conversation
TurboModule return values, promise results, callback arguments and codegen events are converted by convertObjCObjectToJSIValue. Dictionaries now read their keys and values at once instead of looking up each key again, keys are created as property names directly, nested values are retained once per container instead of once per call, and short strings without character storage are copied to the stack instead of the heap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Events sent with RCTEventEmitter went through folly::dynamic: every string was encoded as UTF-8 on the sending thread and decoded again on the JS thread. RCTInstance now reads the arguments on the sending thread into a small tree with the same structure, where strings keep their ASCII or UTF-16 characters, and the JS thread creates the JS values from it. ReactInstance gets a callFunctionOnModule overload that takes an ArgFactory, which creates the arguments on the JS thread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Visual regressionNo visual differences at android: no capture |
Drop CallArgument and convert the arguments on the JS thread with the TurboModule helper, like codegen's AsyncEventEmitter does. The helper now turns number keys into strings and skips other keys that aren't strings, as convertIdToFollyDynamic does, instead of treating every key as an NSString. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@javache makes sense! #59449 now uses |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) | ||
| { | ||
| return convertNSStringToJSI<jsi::String>(runtime, value); | ||
| } |
There was a problem hiding this comment.
Call the templated helper directly in callers of convertNSStringToJSIString
| __unsafe_unretained id stackObjects[kStackCount]; | ||
| std::vector<__unsafe_unretained id> heapObjects; |
There was a problem hiding this comment.
The stack optimization isn't helpful here, since we end having to copy it in the return value anyway.
Just use heapObjects everywhere and return std::move() it
| if (length <= kStackLength) { | ||
| UInt8 ascii[kStackLength]; | ||
| CFIndex asciiLength = 0; | ||
| if (CFStringGetBytes( | ||
| string, CFRangeMake(0, length), kCFStringEncodingASCII, 0, false, ascii, length, &asciiLength) == length) { | ||
| // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) | ||
| return fromAscii(reinterpret_cast<const char *>(ascii), static_cast<size_t>(asciiLength)); | ||
| } | ||
| UniChar characters[kStackLength]; | ||
| CFStringGetCharacters(string, CFRangeMake(0, length), characters); | ||
| return fromUtf16(characters, static_cast<size_t>(length)); | ||
| } |
There was a problem hiding this comment.
Was this path observed to be making things more efficient?
| jsi::Array result = jsi::Array(runtime, value.count); | ||
| for (size_t i = 0; i < value.count; i++) { | ||
| result.setValueAtIndex(runtime, i, convertObjCObjectToJSIValue(runtime, value[i])); | ||
| std::vector<id> objects = getRetainedObjects(value); |
There was a problem hiding this comment.
Why do we need to retain them? The NSArray who the caller is holding on to is already doing so.
…omodule-faster-object-conversion Keeps react#59449's key handling in convertNSDictionaryToJSIObject: number keys become strings, and other keys that aren't strings are skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Call convertNSStringToJSI<jsi::String> directly instead of convertNSStringToJSIString. - Read a container's objects into one vector and return it. - Don't retain the objects: the container holds them while they are converted. - Drop the stack copy of short strings. With it back, the benchmarks were within 1%, so it didn't help. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary:
TurboModuleConvertUtils::convertObjCObjectToJSIValueconverts native values to JS for TurboModule return values, promise results, callback arguments and codegen event emitters.Current behavior. For an
NSDictionary, it enumerates the keys and then looks up each value again withvalue[k]. Each key becomes ajsi::String, which the runtime then turns into a property name. Each nested value is retained and released at every level of the recursion. Short strings that CoreFoundation stores inside the pointer have no character storage, so they are copied into a heap-allocatedstd::u16string.With this change.
getObjects:andKeys:count:, and arrays withgetObjects:range:. The objects are retained once, in a vector, while they are converted, because JS code that runs while properties are set could change the container. Below that, the conversion doesn't retain each value again.jsi::PropNameID::forAsciiorforUtf16.JS receives the same values as before.
Changelog:
[IOS] [CHANGED] - Objects returned from native modules are converted to JS with fewer lookups and retains
Test Plan:
clang-format --dry-run --Werroron the changed file passes.xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1).NSDecimalNumber;NSNull,NSDateandNSURLvalues;NativeSampleTurboModulemethods in a loop. The calls include the unchanged conversion of the arguments from JS. The two arms are the same binary with the conversion switched by a launch argument, in alternating order: 8 launches without and 4 with this change. The presses were driven by Argent on the physical iPhone. iPhone 15 Pro, Release, medians per call:getObject, 20 keysgetObject, nested object like a keyboard eventgetObject, 3 keysgetValue(returns an object)The ranges of the two arms are separate for the first two rows and overlap for the last two. Calls that don't return objects are the same within noise.
🤖 Generated with Claude Code