Skip to content
Permalink
Browse files
Avoid injected bundle delegate calls when text fields are focused and…
… blurred without user interaction

https://bugs.webkit.org/show_bug.cgi?id=240614

Reviewed by Chris Dumez.

Add a mechanism to throttle calls to injected bundle form client via `textFieldDidBeginEditing`, in
the case where the focused element is in a subframe that has never handled an editing command or
user interaction. This yields a small win on Speedometer 2, on M1 MacBookPro:

```
----------------------------------------------------------------------------------------------------------
|               subtest                |     ms      |     ms      |  b / a   | pValue                   |
----------------------------------------------------------------------------------------------------------
| Angular2-TypeScript-TodoMVC          |30.651667    |28.456667    |0.928389  | 0.000000 (significant)   |
| AngularJS-TodoMVC                    |109.733333   |111.623333   |1.017224  | 0.000000 (significant)   |
| BackboneJS-TodoMVC                   |31.371667    |33.988333    |1.083409  | 0.000000 (significant)   |
| Elm-TodoMVC                          |96.818333    |96.893333    |1.000775  | 0.760078                 |
| EmberJS-Debug-TodoMVC                |292.241667   |292.628333   |1.001323  | 0.530967                 |
| EmberJS-TodoMVC                      |99.671667    |98.963333    |0.992893  | 0.040683                 |
| Flight-TodoMVC                       |43.815000    |50.915000    |1.162045  | 0.000000 (significant)   |
| Inferno-TodoMVC                      |46.633333    |44.911667    |0.963081  | 0.000000 (significant)   |
| Preact-TodoMVC                       |11.636667    |11.673333    |1.003151  | 0.862258                 |
| React-Redux-TodoMVC                  |121.411667   |120.096667   |0.989169  | 0.000000 (significant)   |
| React-TodoMVC                        |69.908333    |69.885000    |0.999666  | 0.944581                 |
| Vanilla-ES2015-Babel-Webpack-TodoMVC |47.750000    |46.223333    |0.968028  | 0.000000 (significant)   |
| Vanilla-ES2015-TodoMVC               |48.721667    |48.323333    |0.991824  | 0.001202 (significant)   |
| VanillaJS-TodoMVC                    |40.218333    |38.231667    |0.950603  | 0.000000 (significant)   |
| VueJS-TodoMVC                        |18.420000    |16.793333    |0.911690  | 0.000000 (significant)   |
| jQuery-TodoMVC                       |188.831667   |186.908333   |0.989815  | 0.000005 (significant)   |
----------------------------------------------------------------------------------------------------------

a mean = 343.48012
b mean = 344.99902
pValue = 0.0027314347
(Bigger means are better.)
1.004 times better
Results ARE significant
```

See below for more details.

* Source/WebCore/editing/Editor.cpp:
(WebCore::Editor::Editor):
(WebCore::Editor::stopTextFieldDidBeginEditingTimer):

Stop the `textFieldDidBeginEditing` timer if needed, and return true if and only if it was active.

(WebCore::Editor::textFieldDidBeginEditingTimerFired):

Dispatch the deferred EditorClient call using the currently focused element.

(WebCore::Editor::textFieldDidBeginEditing):

If we're inside of a subframe that has never handled user interaction or editing, then don't eagerly
notify the injected bundle about the newly focused text field; instead, schedule a newly added timer
(`m_textFieldDidBeginEditingTimer`) to perform this call after a short delay.

(WebCore::Editor::textFieldDidEndEditing):

If editing ends (i.e. the text field is blurred) while the `textFieldDidBeginEditing` timer is still
scheduled, then simply elide this call to `textFieldDidBeginEditing` and `textFieldDidEndEditing`
altogether. This prevents us from repeatedly calling into the injected bundle if a page frequently
programmatically focuses and blurs text fields.

(WebCore::Editor::textDidChangeInTextField):
(WebCore::Editor::doTextFieldCommandFromEvent):
(WebCore::Editor::textWillBeDeletedInTextField):
(WebCore::Editor::textDidChangeInTextArea):

If any of these other injected bundle form client hooks are invoked while there is a scheduled
`textFieldDidBeginEditing` timer, then stop the timer and immediately inform the injected bundle
client about the focused text field.

(WebCore::Editor::isInSubframeWithoutUserInteraction const):
(WebCore::Editor::respondToChangedSelection):

Use the new helper function above.

* Source/WebCore/editing/Editor.h:

Canonical link: https://commits.webkit.org/250771@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@294514 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
whsieh committed May 20, 2022
1 parent f0527ad commit 7d2e6e3e079f23b051a968eb1e388a37469806ee
Showing 2 changed files with 86 additions and 19 deletions.
@@ -138,6 +138,8 @@

namespace WebCore {

constexpr auto textFieldDidBeginEditingClientNotificationDelay = 500_ms;

static bool dispatchBeforeInputEvent(Element& element, const AtomString& inputType, const String& data = { }, RefPtr<DataTransfer>&& dataTransfer = nullptr, const Vector<RefPtr<StaticRange>>& targetRanges = { }, Event::IsCancelable cancelable = Event::IsCancelable::Yes)
{
auto event = InputEvent::create(eventNames().beforeinputEvent, inputType, cancelable, element.document().windowProxy(), data, WTFMove(dataTransfer), targetRanges, 0);
@@ -1246,6 +1248,7 @@ Editor::Editor(Document& document)
#if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS_FAMILY)
, m_telephoneNumberDetectionUpdateTimer(*this, &Editor::scanSelectionForTelephoneNumbers, 0_s)
#endif
, m_textFieldDidBeginEditingTimer(*this, &Editor::textFieldDidBeginEditingTimerFired)
{
}

@@ -3462,43 +3465,95 @@ void Editor::computeAndSetTypingStyle(StyleProperties& properties, EditAction ed
return computeAndSetTypingStyle(EditingStyle::create(&properties), editingAction);
}

void Editor::textFieldDidBeginEditing(Element& e)
bool Editor::stopTextFieldDidBeginEditingTimer()
{
if (client())
client()->textFieldDidBeginEditing(e);
if (m_textFieldDidBeginEditingTimer.isActive()) {
m_textFieldDidBeginEditingTimer.stop();
return true;
}
return false;
}

void Editor::textFieldDidBeginEditingTimerFired()
{
auto* client = this->client();
if (!client)
return;

if (RefPtr element = m_document.activeElement())
client->textFieldDidBeginEditing(*element);
}

void Editor::textFieldDidEndEditing(Element& e)
void Editor::textFieldDidBeginEditing(Element& element)
{
auto* client = this->client();
if (!client)
return;

if (isInSubframeWithoutUserInteraction()) {
m_textFieldDidBeginEditingTimer.startOneShot(textFieldDidBeginEditingClientNotificationDelay);
return;
}

client->textFieldDidBeginEditing(element);
}

void Editor::textFieldDidEndEditing(Element& element)
{
dismissCorrectionPanelAsIgnored();
if (client())
client()->textFieldDidEndEditing(e);

auto* client = this->client();
if (!client)
return;

if (stopTextFieldDidBeginEditingTimer())
return;

client->textFieldDidEndEditing(element);
}

void Editor::textDidChangeInTextField(Element& e)
void Editor::textDidChangeInTextField(Element& element)
{
if (client())
client()->textDidChangeInTextField(e);
auto* client = this->client();
if (!client)
return;

if (stopTextFieldDidBeginEditingTimer())
client->textFieldDidBeginEditing(element);
client->textDidChangeInTextField(element);
}

bool Editor::doTextFieldCommandFromEvent(Element& e, KeyboardEvent* ke)
bool Editor::doTextFieldCommandFromEvent(Element& element, KeyboardEvent* event)
{
if (client())
return client()->doTextFieldCommandFromEvent(e, ke);
auto* client = this->client();
if (!client)
return false;

return false;
if (stopTextFieldDidBeginEditingTimer())
client->textFieldDidBeginEditing(element);
return client->doTextFieldCommandFromEvent(element, event);
}

void Editor::textWillBeDeletedInTextField(Element& input)
{
if (client())
client()->textWillBeDeletedInTextField(input);
auto* client = this->client();
if (!client)
return;

if (stopTextFieldDidBeginEditingTimer())
client->textFieldDidBeginEditing(input);
client->textWillBeDeletedInTextField(input);
}

void Editor::textDidChangeInTextArea(Element& e)
void Editor::textDidChangeInTextArea(Element& element)
{
if (client())
client()->textDidChangeInTextArea(e);
auto* client = this->client();
if (!client)
return;

if (stopTextFieldDidBeginEditingTimer())
client->textFieldDidBeginEditing(element);
client->textDidChangeInTextArea(element);
}

void Editor::applyEditingStyleToBodyElement() const
@@ -3686,6 +3741,11 @@ void Editor::selectionWillChange()
}
#endif

bool Editor::isInSubframeWithoutUserInteraction() const
{
return !m_hasHandledAnyEditing && !m_document.hasHadUserInteraction() && !m_document.isTopDocument();
}

void Editor::respondToChangedSelection(const VisibleSelection&, OptionSet<FrameSelection::SetSelectionOption> options)
{
#if PLATFORM(IOS_FAMILY)
@@ -3705,7 +3765,7 @@ void Editor::respondToChangedSelection(const VisibleSelection&, OptionSet<FrameS
setStartNewKillRingSequence(true);
m_imageElementsToLoadBeforeRevealingSelection.clear();

if (!m_hasHandledAnyEditing && !m_document.hasHadUserInteraction() && !m_document.isTopDocument())
if (isInSubframeWithoutUserInteraction())
return;

if (m_editorUIUpdateTimer.isActive())
@@ -632,6 +632,8 @@ class Editor {

std::optional<SimpleRange> adjustedSelectionRange();

bool isInSubframeWithoutUserInteraction() const;

#if PLATFORM(COCOA)
RefPtr<SharedBuffer> selectionInWebArchiveFormat();
String selectionInHTMLFormat();
@@ -647,6 +649,9 @@ class Editor {
void notifyClientOfAttachmentUpdates();
#endif

bool stopTextFieldDidBeginEditingTimer();
void textFieldDidBeginEditingTimerFired();

String platformContentTypeForBlobType(const String& type) const;

void postTextStateChangeNotificationForCut(const String&, const VisibleSelection&);
@@ -690,6 +695,8 @@ class Editor {
Vector<SimpleRange> m_detectedTelephoneNumberRanges;
#endif

Timer m_textFieldDidBeginEditingTimer;

mutable std::unique_ptr<ScrollView::ProhibitScrollingWhenChangingContentSizeForScope> m_prohibitScrollingDueToContentSizeChangesWhileTyping;

bool m_isGettingDictionaryPopupInfo { false };

0 comments on commit 7d2e6e3

Please sign in to comment.