Repository navigation
Convert iOS native module objects to JS with fewer lookups and retains - #59450
hryhoriiK97 wants to merge 2 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 <[email protected]>
Visual regressionNo visual differences at android: no capture |
@javache makes sense! #59449 now uses |
| 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?
There was a problem hiding this comment.
Not on its own, so I measured it now: putting it back on top of the reworked version is within ±1% on every benchmark (20-key, nested, 3-key objects), even though those keys are short tagged-pointer strings. So I removed it - da67a6c
| 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.
There was a problem hiding this comment.
I was worried about JS changing the container while properties are set, but that would need a setter on Object.prototype. It's unretained now - da67a6c
- 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 <[email protected]>
08a67d0 to
da67a6c
Compare
|
@javache has imported this pull request. If you are a Meta employee, you can view this in D127788555. |
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.With this change.
getObjects:andKeys:count:, and arrays withgetObjects:range:. The objects aren't retained again: the container holds them while they are converted.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:
xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1). The measured build had this change on top of Convert iOS native module event arguments to JS without UTF-8 #59449, so both arms below include Convert iOS native module event arguments to JS without UTF-8 #59449's key handling.NSDecimalNumber;NSNull,NSDateandNSURLvalues;NSNullkeys;NativeSampleTurboModulemethods in a loop: 5 rounds of 20,000 calls per method on each launch. The calls include the unchanged conversion of the arguments from JS. The arms are the same binary with the conversion switched by a launch argument, 4 launches each, in rotating order. 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. For the last two they overlap only because of one launch of the current code. A version that also copied short strings to the stack was within 1% of this one on every row, so it isn't included.
clang-format, which needsdotslash.🤖 Generated with Claude Code