Bug 220502 - Radio buttons with no form owner are not grouped
Summary: Radio buttons with no form owner are not grouped
Status: RESOLVED FIXED
Alias: None
Product: WebKit
Classification: Unclassified
Component: Forms (show other bugs)
Version: Safari Technology Preview
Hardware: Unspecified macOS 10.15
: P2 Normal
Assignee: Chris Dumez
URL: https://chromium-review.googlesource....
Keywords: InRadar
: 233847 (view as bug list)
Depends on: 235645 235718
Blocks: 233428
  Show dependency treegraph
 
Reported: 2021-01-10 11:52 PST by Matt
Modified: 2022-01-28 00:00 PST (History)
15 users (show)

See Also:


Attachments
WIP Patch (13.24 KB, patch)
2021-12-06 09:58 PST, Chris Dumez
ews-feeder: commit-queue-
Details | Formatted Diff | Diff
WIP Patch (14.33 KB, patch)
2021-12-06 11:44 PST, Chris Dumez
ews-feeder: commit-queue-
Details | Formatted Diff | Diff
WIP Patch (16.45 KB, patch)
2021-12-08 14:57 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (23.31 KB, patch)
2021-12-08 15:44 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (26.24 KB, patch)
2021-12-09 07:33 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (30.13 KB, patch)
2021-12-09 12:43 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (31.27 KB, patch)
2021-12-09 13:06 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch for landing (31.27 KB, patch)
2021-12-10 07:49 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (28.77 KB, patch)
2022-01-26 12:05 PST, Chris Dumez
no flags Details | Formatted Diff | Diff
Patch (28.33 KB, patch)
2022-01-27 14:44 PST, Chris Dumez
no flags Details | Formatted Diff | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Matt 2021-01-10 11:52:20 PST
I created a series of tests to address issue #2551 (https://github.com/web-platform-tests/wpt/issues/2551) of the Web Platform Tests project. One of the tests is failing when run against Safari 118 Preview. The test consists of two radio button inputs that should be grouped according to the criteria in the HTML spec (type=radio, same tree, identical name attribute values, and no form owner).

The test results can be viewed in the Safari wpt.fyi link for the WPT pull request (https://github.com/web-platform-tests/wpt/pull/27029). The relevant test is labeled "Radio buttons in different groups (because they have different form owners or no form owner) do not affect each other's checkedness".

The test reveals that two radio buttons meeting all the criteria to be in the same group (type=radio, same tree, identical name attribute values, and no form owner) are not being properly grouped. The checked statuses of the two buttons are not mutually exclusive.

Link to the test results is https://wpt.fyi/results/html/semantics/forms/the-input-element/radio.html?diff&filter=ADC&run_id=5710239401771008&run_id=5647309809385472
Comment 1 Radar WebKit Bug Importer 2021-01-17 11:53:12 PST
<rdar://problem/73300895>
Comment 2 Smoley 2021-01-20 16:48:49 PST
Thanks for filing, I can reproduce this on Safari 13.1.3, STP118 and Firefox using http://wpt.live/html/semantics/forms/the-input-element/radio.html. This does not reproduce on Chrome.
Comment 3 Chris Dumez 2021-12-06 09:34:32 PST
*** Bug 233847 has been marked as a duplicate of this bug. ***
Comment 4 Chris Dumez 2021-12-06 09:58:22 PST
Created attachment 446048 [details]
WIP Patch
Comment 5 Chris Dumez 2021-12-06 11:44:12 PST
Created attachment 446062 [details]
WIP Patch
Comment 6 Chris Dumez 2021-12-08 14:57:52 PST
Created attachment 446431 [details]
WIP Patch
Comment 7 Chris Dumez 2021-12-08 15:44:21 PST
Created attachment 446443 [details]
Patch
Comment 8 Chris Dumez 2021-12-09 07:33:11 PST
Created attachment 446548 [details]
Patch
Comment 9 Darin Adler 2021-12-09 11:37:51 PST
Comment on attachment 446548 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=446548&action=review

> Source/WebCore/html/HTMLInputElement.cpp:1617
> +    if (removalType.disconnectedFromDocument && !form() && isRadioButton())
> +        updateValidity();

Maybe it’s obvious, but why can we skip this update if form() is non-null?

> Source/WebCore/html/HTMLInputElement.cpp:1967
> +    if (checked())
> +        return const_cast<HTMLInputElement*>(this);

I guess this is an optimization. Or is it for correctness?

> Source/WebCore/html/HTMLInputElement.cpp:1980
> +    // The input is not managed by a RadioButtonGroups, we'll need to traverse the tree.
> +    for (auto& descendant : descendantsOfType<HTMLInputElement>(rootNode())) {
> +        if (descendant.isRadioButton() && name == descendant.name() && !descendant.form() && descendant.checked())
> +            return &descendant;
> +    }

I understand that the Chromium engineer who wrote this code was OK with the performance cost of walking the tree every time. But we are OK with that? What is the pathological worst case version of this?

Also, the checked() function is a super-fast one, so maybe it should go earlier in this clause. Faster than isRadioButton (which calls a virtual function), faster than a string comparison, even faster than the really simple form function call (which is not inlined).

> Source/WebCore/html/RadioInputType.cpp:49
> +    auto name = element()->name();

Maybe auto& to avoid some reference count churn.

> Source/WebCore/html/RadioInputType.cpp:59
> +    auto& treeRoot = element()->rootNode();
> +    for (auto* input = Traversal<HTMLInputElement>::inclusiveFirstWithin(treeRoot); input; input = Traversal<HTMLInputElement>::next(*input, &treeRoot)) {

I’d rather have this use inclusiveDescendantsOfType, iterator style. You’d have to add that, though. It would go in TypedDescendantsIterator.h:

    for (auto& input : inclusiveDescendantsOfType<HTMLInputElement>(element()->rootNode())) {

Same question as above about the cost of walking the tree every time.

> Source/WebCore/html/RadioInputType.cpp:67
> +        if (isChecked && isRequired)
> +            return false;

We can return false as soon as *isChecked* is true, doesn’t matter what the state of *isRequired* is. So we don’t need an isChecked boolean, just return false as soon as we find a checked one.
Comment 10 Chris Dumez 2021-12-09 12:08:27 PST
Comment on attachment 446548 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=446548&action=review

>> Source/WebCore/html/HTMLInputElement.cpp:1617
>> +        updateValidity();
> 
> Maybe it’s obvious, but why can we skip this update if form() is non-null?

If the radio button has an associated form, then it is part of a RadioButtonGroup (held by the form element) and RadioButtonGroups properly maintain radio buttons validity.

The issue is for radio buttons that are both disconnected AND have no form element. In this case, the radio button is no longer part of a RadioButtonGroup and we need to traverse its detached dom tree to figure out its validity.

>> Source/WebCore/html/HTMLInputElement.cpp:1967
>> +        return const_cast<HTMLInputElement*>(this);
> 
> I guess this is an optimization. Or is it for correctness?

This is an optimization. That said, it I dropped this optimization, I would need to also use inclusiveDescendantsOfType<> below instead of descendantsOfType<>.

>> Source/WebCore/html/HTMLInputElement.cpp:1980
>> +    }
> 
> I understand that the Chromium engineer who wrote this code was OK with the performance cost of walking the tree every time. But we are OK with that? What is the pathological worst case version of this?
> 
> Also, the checked() function is a super-fast one, so maybe it should go earlier in this clause. Faster than isRadioButton (which calls a virtual function), faster than a string comparison, even faster than the really simple form function call (which is not inlined).

It is true that traversing the DOM tree is a bit unfortunate. That said, this only occurs in the case where the input element is detached from the tree. The bad case is when you have a radio button that is part of a large detached subtree (e.g. in a document fragment).
I am currently A/B testing on Speedometer to make sure it has no impact there but I don't expect this to be a common case in practice.

>> Source/WebCore/html/RadioInputType.cpp:49
>> +    auto name = element()->name();
> 
> Maybe auto& to avoid some reference count churn.

OK.

>> Source/WebCore/html/RadioInputType.cpp:59
>> +    for (auto* input = Traversal<HTMLInputElement>::inclusiveFirstWithin(treeRoot); input; input = Traversal<HTMLInputElement>::next(*input, &treeRoot)) {
> 
> I’d rather have this use inclusiveDescendantsOfType, iterator style. You’d have to add that, though. It would go in TypedDescendantsIterator.h:
> 
>     for (auto& input : inclusiveDescendantsOfType<HTMLInputElement>(element()->rootNode())) {
> 
> Same question as above about the cost of walking the tree every time.

Good idea, I'll look into adding an inclusiveDescendantsOfType().

>> Source/WebCore/html/RadioInputType.cpp:67
>> +            return false;
> 
> We can return false as soon as *isChecked* is true, doesn’t matter what the state of *isRequired* is. So we don’t need an isChecked boolean, just return false as soon as we find a checked one.

Oh, that is a very good point. I completely missed that!
I'll fix.
Comment 11 Chris Dumez 2021-12-09 12:43:45 PST
Created attachment 446591 [details]
Patch
Comment 12 Chris Dumez 2021-12-09 12:47:02 PST
Still waiting on perf A/B results.
Comment 13 Darin Adler 2021-12-09 12:56:26 PST
Comment on attachment 446591 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=446591&action=review

> Source/WebCore/html/RadioInputType.cpp:51
> +        return buttons->isInRequiredGroup(*element()) && !buttons->checkedButtonForGroup(name);

Reverse the order of checks here if the second is less expensive?

> Source/WebCore/html/RadioInputType.cpp:63
> +        if (input.isRequired())
> +            isRequired = true;

If isRequired is costly, could call it only if boolean is not already true.
Comment 14 Chris Dumez 2021-12-09 13:05:34 PST
(In reply to Darin Adler from comment #13)
> Comment on attachment 446591 [details]
> Patch
> 
> View in context:
> https://bugs.webkit.org/attachment.cgi?id=446591&action=review
> 
> > Source/WebCore/html/RadioInputType.cpp:51
> > +        return buttons->isInRequiredGroup(*element()) && !buttons->checkedButtonForGroup(name);
> 
> Reverse the order of checks here if the second is less expensive?

Ok.

> 
> > Source/WebCore/html/RadioInputType.cpp:63
> > +        if (input.isRequired())
> > +            isRequired = true;
> 
> If isRequired is costly, could call it only if boolean is not already true.

isRequired() just returns a boolean data member. However, it is currently out-of-line, I think I should just inline it.
Comment 15 Chris Dumez 2021-12-09 13:06:12 PST
Created attachment 446595 [details]
Patch
Comment 16 Chris Dumez 2021-12-10 07:31:12 PST
Comment on attachment 446595 [details]
Patch

Perf-neutral on Speedometer
Comment 17 Chris Dumez 2021-12-10 07:48:03 PST
Comment on attachment 446595 [details]
Patch

View in context: https://bugs.webkit.org/attachment.cgi?id=446595&action=review

> Source/WebCore/html/RadioInputType.cpp:49
> +    auto name = element()->name();

Forgot to switch to auto& here. Will address.
Comment 18 Chris Dumez 2021-12-10 07:49:17 PST
Created attachment 446728 [details]
Patch for landing
Comment 19 EWS 2021-12-10 09:18:11 PST
Committed r286855 (245088@main): <https://commits.webkit.org/245088@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 446728 [details].
Comment 20 Chris Dumez 2022-01-26 11:19:50 PST
Reopening since this got reverted in https://commits.webkit.org/246446@main.
Comment 21 Chris Dumez 2022-01-26 12:05:37 PST
Created attachment 450052 [details]
Patch
Comment 22 Chris Dumez 2022-01-27 14:44:36 PST
Created attachment 450182 [details]
Patch
Comment 23 EWS 2022-01-28 00:00:10 PST
Committed r288734 (246527@main): <https://commits.webkit.org/246527@main>

All reviewed patches have been landed. Closing bug and clearing flags on attachment 450182 [details].