-
Notifications
You must be signed in to change notification settings - Fork 6k
Removed superfluous copy operations in darwin flutter platform messages by wrapping data in custom NSData classes. #25702
Removed superfluous copy operations in darwin flutter platform messages by wrapping data in custom NSData classes. #25702
Conversation
| } | ||
|
|
||
| + (NSData*)dataWithMapping:(std::unique_ptr<fml::Mapping>)mapping { | ||
| return (NSData*)[[[FlutterMappingData alloc] initWithMapping:std::move(mapping)] autorelease]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could selectively just copy the data here if the buffer is small enough.
|
|
||
| // Const cast is required because the NSData API requires it despite | ||
| // guarentees that the buffer won't be deleted or modified. | ||
| _data = [[NSData alloc] initWithBytesNoCopy:const_cast<void*>(rawData) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an extra allocation that wouldn't be happening before in lieu of a copy.
|
@stuartmorgan @xster I was trying to figure out how platform channels work completely. When looking into it I saw we were doing an extra copy of the data to get NSData's for passing to the user. This PR removes two of those extra copies. We've had some users complain about the performance of sending large buffers so this will help with that (@xster do we have an issue open for that?). This technically could be slower if the buffers are small since it has an extra allocation and one layer of indirection when accessing the bytes. We could get around that by selectively copying if the buffers are small. We'd have to do some microbenchmarks to get an idea about the math for that. While looking I've also found an opportunity to speed up android platform channels by using direct buffers. That sounds worthwhile, what do you think? We could also potentially remove the copies that are happening in CopyNSDataToVector and CopyNSDataToMapping if we transitioned away from using std::vector and instead used our own buffer wrapper that can do no-copy initializations and releases. That might be a bit more involved though. |
Yes I think so! The users that brought this issue up were testing on Android. |
|
|
||
| /// A proxy object that behaves like NSData represented in a Mapping. | ||
| /// This isn't a subclass of NSData because NSData is in a class cluster. | ||
| @interface FlutterMappingData : NSObject |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this inheriting from NSObject rather than NSProxy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because NSObject gives us forwardingTargetForSelector, the NSProxy is slower. Check out the documentation on forwardingTargetForSelector https://developer.apple.com/documentation/objectivec/nsobject/1418855-forwardingtargetforselector
| - (id)forwardingTargetForSelector:(SEL)aSelector { | ||
| return _data; | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's been a while since I've written a proxy object, but IIRC without several other method overrides (e.g., respondsToSelector), this won't actually look like an NSData in various circumstances.
You might argue that those are edge cases, but this is being passed to arbitrary third-party code, and claims to be an NSData; those edge cases are all valid, and we have no way to guarantee that they don't exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The respondsToSelector+forwardInvocation is a different slower forwarding mechanism.
Yea, if the code ever did reflection on the NSData it could be a problem. Subclassing class clusters isn't clear, they really push forwarding in the documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correctness is more important that speed though, and I find it very unlikely that the cost of calls to this "NSData" object is going to even more than a tiny, tiny fraction of the end-to-end performance of platform channels.
(Edited to add: to be clear, by "cost of calls" I mean the actual cost of doing the method invocation, including forwarding, not the overall operations like the copy you are trying to eliminate.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was mistaken, because NSData is a class cluster this isn't actually a problem. Reflection doesn't work on proper NSData classes anyways. If you do the following:
#include <Foundation/Foundation.h>
int main() {
NSData* data = [[NSData alloc] init];
NSLog(@"%@", data.class);
}it prints out _NSZeroData. You can't do reflection on NSData so this is as correct as can be.
That said, I'm still going to investigate the alternative we discussed since it is cleaner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't do reflection on NSData
You've demonstrated one specific call. What about isKindOfClass:? And much more importantly, how does your implementation handle respondsToSelector?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, I'd have to override respondsToSelector. To get it to respond to isKindOfClass: I'd have to either override all methods, or fiddle with the objc_class. I put a pin in this while I research the broader question: https://docs.google.com/document/d/1oNLxJr_ZqjENVhF94-PqxsGPx0qGXx-pRJxXL6LSagc/edit
| NSData* data = nil; | ||
| if (message->hasData()) { | ||
| data = GetNSDataFromVector(message->data()); | ||
| data = [FlutterMessageData dataWithMessage:std::move(message)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like it would be much safer to change PlatformMessage to allow for extracting the data with ownership, so we could actually just construct a no-copy NSData here, rather than using a proxy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, you're probably right. I'll have to investigate, they are using shared pointers for the platformmessage, but I think they are essentially unique_ptr's. If they are truly unique_ptr's we can steal ownership of the buffer. It might help us on the android side too. I can look into this today
1c941ac to
5d01db9
Compare
|
@stuartmorgan Okay, I spent some time researching this and trying out what we discussed. This is the right approach, the alternative is a massive breaking change: #25867. While that approach is a good idea and would allow us to eliminate all 4 copies of the message data, it percolates all the way up to This approach will eliminate 3/4 copies and is much less invasive/risky/controversial. I've implemented performance tests so I recommend we do this and reevaluate if we want to do the breaking change to geek out the last copy. (edit: the remaining copy that couldn't be reviewed is in the Host->Flutter direction for Host originating messages, so it doesn't affect plugins fwiw). I implemented the I believe we'll be able to do something similar for JNI, too. I don't think the Mapping breaking change is needed to eliminate the 3 copies on Android either. |
| #include <vector> | ||
|
|
||
| #include "flutter/fml/mapping.h" | ||
| #import "flutter/lib/ui/window/platform_message.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be removed if my other PR lands where I migrate PlatformMessage to std::unique_ptr, then I could steal its std::vector instead of wrapping the PlatformMessage.
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat. If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
|
I've been able to measure this now that we have performance tests and it doesn't actually make anything faster. For standard message codecs it seems the serialization time dwarfs copying time. For binary codecs the majority of time is in the deallocator for the std::vector. I'm not sure why yet, but regardless this PR isn't worth considering until it can prove it can deliver performance, dropping back to draft. |
|
FWIW I profiled the other fix and it seems like eliminating the copy didn't show up in 14k payloads, but it did in 1mb payloads. The mapping also had the added benefit of eliminating std::vector deallocation which was heavy. |

issue: flutter/flutter#81559
Pre-launch Checklist
writing and running engine tests.
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.