-
Notifications
You must be signed in to change notification settings - Fork 6k
[iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop #43267
Conversation
1a60079 to
a02dcc7
Compare
|
Some observations: When typing a sequence that results in an em-dash: I also tried to observe what a native UITextField does in this situation by listening to the textDidChange event. I only observed one textDidChange call when doing an em-dash transformation. [self.textField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; |
justinmc
left a comment
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.
LGTM but @LongCatIsLooong should definitely take a look.
The run loop thing looks like an elegant solution to this problem. It seems like that's how things like the em dash are supposed to be handled.
| - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text { | ||
| // `temporarilyDeletedComposedCharacter` should only be used during a single text change session. | ||
| // So it needs to be cleared at the start of each text editting session. | ||
| // So it needs to be cleared at the start of each text editing session. |
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.
🕵️
| done = false; | ||
| XCTAssertFalse(done); | ||
| CFRunLoopPerformBlock(CFRunLoopGetMain(), (__bridge CFStringRef)runLoopMode, ^{ | ||
| done = true; | ||
| }); | ||
|
|
||
| while (!done) { | ||
| // Each invocation will handle one source. | ||
| CFRunLoopRunInMode((__bridge CFStringRef)runLoopMode, 0, true); | ||
| } | ||
| XCTAssertTrue(done); |
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.
Would it be any cleaner to abstract this code to a function or something? It's kind of a bummer that we have to clear the runloop all over the place like this, but it makes sense. The next time a test is added here, it should be obvious that this is necessary since it's all over.
| _markedRect = kInvalidFirstRect; | ||
| _cachedFirstRect = kInvalidFirstRect; | ||
| _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone; | ||
| _pendingDeltas = [[NSMutableArray alloc] init]; |
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.
Do you have to release this somewhere? I'm just guessing since I'm no objective c expert.
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.
No release here is needed as this file is ARC.
|
Please wait for @cyanglaz to review as well for ObjC expertise. |
| _markedRect = kInvalidFirstRect; | ||
| _cachedFirstRect = kInvalidFirstRect; | ||
| _scribbleInteractionStatus = FlutterScribbleInteractionStatusNone; | ||
| _pendingDeltas = [[NSMutableArray alloc] init]; |
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.
No release here is needed as this file is ARC.
| @property(nonatomic, copy) NSDictionary* markedTextStyle; | ||
| @property(nonatomic, weak) id<UITextInputDelegate> inputDelegate; | ||
| @property(nonatomic, strong) NSMutableArray* pendingDeltas; | ||
| @property(nonatomic, readwrite, copy) NSString* customRunLoopMode; |
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.
nit: readwrite is the default so it is not necessary here.
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 looks like you don't need to expose the run loop mode? IIRC in the macOS implementation it was only exposed because there was a leak in the tests so we couldn't use async expectations.
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.
Now that I think about it, do you need to use the runloop API? dispatch_async does the same thing right?
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.
Yes I think dispatch_async should accomplish the same thing and we wouldn't need to set the mode either. I can try that.
|
|
||
| [self.textInputDelegate flutterTextInputView:self | ||
| updateEditingClient:_textInputClient | ||
| withDelta:deltas]; |
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 think it is fine but we should confirm that the framework can handle any delta batches. cc @LongCatIsLooong
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.
Are you referring to the frameworks ability to receive/apply delta batches?
| NSDictionary* deltas = @{ | ||
| @"deltas" : @[ deltaToFramework ], | ||
| }; | ||
| [_pendingDeltas addObject:deltaToFramework]; |
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.
Now that there's batching, what happens if oldText is different in the same batch? For example when there's a framework change happened in between?
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.
Do you mean in this situation:
Batch: Insertion, Deletion, Insertion
After first insertion delta is applied, the framework changes the editing state in response to the insertion?
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.
If the engine applies an update from the framework, after the block is scheduled and before the block is invoked. In theory that can happen right?
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 see what you mean. In theory that can happen something like the following example.
Type A followed by B:
Engine types A.
Engine schedules delta block to run.
Framework types C for x reason.
Engine has C.
Engine types B.
Engine has CB and sends to framework, Expected ACB?
Deltas sent:
InsertionA, oldText: ''
InsertionB, oldText:'C'.
Here the user will not really be able to recover. I don't think this is too different from the IME-Framework out of sync issues we usually face. What do you think about creating some method that signals the framework that a batch has been scheduled, and that it probably is not safe to make any framework side changes during this time.
Another thing to note is that Android already does batching of TextEditingDeltas on its TextInputPlugin, so maybe this is an issue we already face on that end.
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.
In your example the batch is going to be partially applied by the framework right? The framework will apply changes in the batch until the point where oldText mismatches? Shouldn't it be all or nothing, when there's on inconsistent "oldText", when framework rejects the entire batch?
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 think the framework will ignore any difference in the oldText. https://github.com/flutter/flutter/blob/f468f3366c26a5092eb964a230ce7892fda8f2f8/packages/flutter/lib/src/services/text_editing_delta.dart#L324-L334 . A TextEditingDelta applies the delta to the oldText to follow a last write wins policy. A user can still prevent the application of a delta by checking the oldText with their current value themselves before calling apply. A user can make it all or nothing but we don't do this by default.
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.
Sorry missed the reply.
A user can make it all or nothing but we don't do this by default
So the current implementation in the framework doesn't guarantee that a delta batch is atomically applied? That sounds like the same issue could still happen if a framework edit is sent to the engine while the engine is replacing a double-dash?
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.
Oh the framework edit is going to be completely ignored in that case?
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.
So the current implementation in the framework doesn't guarantee that a delta batch is atomically applied? That sounds like the same issue could still happen if a framework edit is sent to the engine while the engine is replacing a double-dash?
Yes in theory a framework edit can still come in during the batch accumulation and interrupt the double-dash replacement. I don't think we can avoid this case but I don't think it is less likely. Before this change the double-dash replacement sequence would come in as 3 separate delta batches, so the user could potentially react to the 1st delta batch and make some edit and disrupt the sequence. After this change the sequence comes in as one delta batch, so the user can avoid making an edit until they fully apply the delta batch.
Oh the framework edit is going to be completely ignored in that case?
No, it is still applied by the engine. In the example I stated above the edit that is ignored is the 1st engine edit that types 'A'.
| @property(nonatomic, copy) NSDictionary* markedTextStyle; | ||
| @property(nonatomic, weak) id<UITextInputDelegate> inputDelegate; | ||
| @property(nonatomic, strong) NSMutableArray* pendingDeltas; | ||
| @property(nonatomic, readwrite, copy) NSString* customRunLoopMode; |
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 looks like you don't need to expose the run loop mode? IIRC in the macOS implementation it was only exposed because there was a leak in the tests so we couldn't use async expectations.
| : kCFRunLoopCommonModes; | ||
|
|
||
| CFRunLoopPerformBlock(CFRunLoopGetMain(), runLoopMode, ^{ | ||
| if (_pendingDeltas.count > 0) { |
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.
Is it possible for the text input plugin to already be released when the block is invoked?
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'm not sure, i'm not too familiar with objc memory management. I would think it is possible since the block is run asynchronously. Is this something we should handle?
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.
use __weak maybe?
c316d2b to
f2ed64a
Compare
| __weak FlutterTextInputView* weakSelf = self; | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| __strong FlutterTextInputView* strongSelf = weakSelf; | ||
| NSAssert(strongSelf, |
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 think this is something that can happen legitimately?
| }]; | ||
| } | ||
|
|
||
| - (void)dispatchAsyncOnMain { |
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.
flushScheduledAsyncBlocks, or some such?
| [inputView insertText:@"—"]; | ||
|
|
||
| [self dispatchAsyncOnMain]; | ||
| // __block bool done = false; |
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.
remove commented code here?
|
May I add the autosubmit tag? |
…atch on the next runLoop (flutter/engine#43267)
flutter/engine@284771d...196474b 2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from 20108acef7d2 to d3a7efb77af3 (2 revisions) (flutter/engine#44082) 2023-07-27 rmolivares@renzo-olivares.dev [iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop (flutter/engine#43267) 2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from b522808eb0af to 20108acef7d2 (3 revisions) (flutter/engine#44079) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jonahwilliams@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…1443) flutter/engine@284771d...196474b 2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from 20108acef7d2 to d3a7efb77af3 (2 revisions) (flutter/engine#44082) 2023-07-27 rmolivares@renzo-olivares.dev [iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop (flutter/engine#43267) 2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from b522808eb0af to 20108acef7d2 (3 revisions) (flutter/engine#44079) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jonahwilliams@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…1443) flutter/engine@284771d...196474b 2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from 20108acef7d2 to d3a7efb77af3 (2 revisions) (flutter/engine#44082) 2023-07-27 rmolivares@renzo-olivares.dev [iOS] TextInputPlugin should batch TextEditingDeltas and dispatch on the next runLoop (flutter/engine#43267) 2023-07-27 skia-flutter-autoroll@skia.org Roll Skia from b522808eb0af to 20108acef7d2 (3 revisions) (flutter/engine#44079) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jonahwilliams@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…the next runLoop (flutter#43267) Fixes flutter/flutter#129061
Fixes flutter/flutter#129061
Pre-launch Checklist
///).