8388378: [Regression] FX CSS: styled property is reset to its initial value on a pseudo-class-only transition after tree items change (TreeTableRow -fx-indent collapses) - #2218
Conversation
|
👋 Welcome back jhendrikx! A progress list of the required criteria for merging this PR into |
|
@hjohn This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be: You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been 24 new commits pushed to the
As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details. ➡️ To integrate this PR with the above commit message to the |
|
The total number of required reviews for this PR has been set to 2 based on the presence of this label: |
Webrevs
|
Maran23
left a comment
There was a problem hiding this comment.
Overall, this looks good. Nice to see some documentation for the CssStyleHelper and related classes! Tests look nice as well and you explained all scenarios very well.
And it is great to see the removal of forceSlowpath, which during my debugging was hard for me to understand why it even exists.
Will do more testing later this week!
| /** | ||
| * | ||
| * Caches the calculated value of each styleable property a specific combination of | ||
| * pseudo-class states and font size (see {@link Key}). An entry is shared by every |
There was a problem hiding this comment.
Minor: Two spaces.
| * pseudo-class states and font size (see {@link Key}). An entry is shared by every | |
| * pseudo-class states and font size (see {@link Key}). An entry is shared by every |
There was a problem hiding this comment.
some people write two spaces after the period (I do). but a) it's an internal class and b) even if it were a public javadoc, two spaces will not render in html.
| * {@code Styleable} that has that same combination. | ||
| * <p> | ||
| * {@link #get(String)} returning {@code null} means only that no one has evaluated that | ||
| * property for this entry yet; it should never be treated as "no style applies"(!). This |
There was a problem hiding this comment.
Minor: Grammar
| * property for this entry yet; it should never be treated as "no style applies"(!). This | |
| * property for this entry yet; it should never be treated as "no style applies"(!). This is |
| * The cached value is still useful for others sharing this entry. | ||
| */ | ||
|
|
||
| if (!cssMetaData.isSettable(node)) continue; |
There was a problem hiding this comment.
I would prefer this style, but again, minor and is all of the place in this file anyway
| if (!cssMetaData.isSettable(node)) continue; | |
| if (!cssMetaData.isSettable(node)) { | |
| continue; | |
| } |
| other.attachExtraProperty(); | ||
|
|
||
| /* | ||
| * The "other" dummy control has the extra property attached to it immediately |
There was a problem hiding this comment.
Those inline doc comments are very helpful!
One idea: I wonder if we should put (most of) them at the test method as javadoc.
This is up to you, just a potential suggestion.
It existed for when CSS metadata changed for a Node (so almost specifically to support what If it didn't do this, then a newly discovered CSS property would not be added to the shared cache entry, and then its absence would later be interpreted as "unstyled" while it really was simply "not evaluated". This problem is gone in the new implementation because absence is always treated as "must be re-evaluated", resulting in either a
Thanks, I appreciate the quick review! |
Ah that makes sense. Sounds hacky, as you also described in your ticket. So this new behavior is always more correct. |
Yeah -- before it was actually trivial to get the CSS system confused... all you had to do is bind a property, and then change to a pseudo class state combination that it hadn't seen before yet. It would create a new cache entry, but would omit the "locked" property. If a sibling later switched to the same pseudo class state combination, it would use the cache entry with the omitted property and incorrectly reset it... That was the first problem I investigated, but it turned out the problem from the ticket was even more complex (but similar in nature). I understand that when the CSS system was designed, they probably made the assumption that getCssMetaData is immutable (and it is in most cases); then when that assumption failed because of controls and skins, a |
andy-goryachev-oracle
left a comment
There was a problem hiding this comment.
I like the removal of all this conditionals around the caching, the new code is much more straightforward. Found a couple of possible issues (inline).
One persistent thought I had is whether or not it is possible to have a more formalized approach to testing the CSS subsystem. Like, exhaustively enumerate all possible transitions and develop a test for each.
| /** | ||
| * | ||
| * Caches the calculated value of each styleable property a specific combination of | ||
| * pseudo-class states and font size (see {@link Key}). An entry is shared by every |
There was a problem hiding this comment.
some people write two spaces after the period (I do). but a) it's an internal class and b) even if it were a public javadoc, two spaces will not render in html.
| /* | ||
| * JDK-8127435: If there is no style for the property (SKIP), then check if it must be reset | ||
| * to its initial value. Otherwise, continue with CSS application. | ||
| */ |
There was a problem hiding this comment.
minor: I would have used // style comments
|
|
||
| final String property = cssMetaData.getProperty(); | ||
|
|
||
| CalculatedValue calculatedValue = cacheEntry.get(property); |
There was a problem hiding this comment.
it looks like the value is stored by property name, which means if the SKIP value was stored and then the metadata was replaced (by setting a skin, for example, which uses the same property names), the old SKIP would prevent re-evaluation.
There was a problem hiding this comment.
A property always evaluates the same, as this is dictated by the stylesheet, not by the presence or absence of a property in the CSS metadata. So the stylesheet says: xyz must be 5 here, and if you replace the skin and it has the same property, that evaluation is still correct.
There may be a problem if that new property is of a different type (I didn't test), but I consider that beyond the scope of this fix as that has never worked. If you see an easy solution, I can consider adding it -- my primary goal was to fix the customer found regression that seemed to have been due to #1076 but in reality has been there much longer, only less visible.
There was a problem hiding this comment.
I have to admit it's a rather contrived scenario, so I am not concerned. The devs can always work around this by using different property names.
Maybe just add a comment pointing to this scenario?
There was a problem hiding this comment.
I added this to StyleCacheEntry:
Entries are (currently) keyed purely by property name, with no check that a cached value's type still matches what the current lookup expects. This is harmless as long as every {@code Styleable} that could share an entry agrees on the type behind a given property name (which is generally standard for CSS properties). It is not a problem for two different {@code CssMetaData} instances to have the same property name, only if their types would also differ.
|
|
||
| /* | ||
| * The other node is now transitioned to the same state. Since the value of -fx-boom | ||
| * could not be evaluated, the value is reset to its default value (1.0). |
There was a problem hiding this comment.
shouldn't it be the value specified by the stylesheet? 41.0, since the styling of the other has not encountered an error?
There was a problem hiding this comment.
You are right that 41.0 would make more sense here, but that's assuming we could get that value at all. The source of the exception used here is just to test the SKIP adding code path (as it is crucial now that we add SKIP or a value for every value in the CSS metadata). The path I was testing however is normally only triggered when lookup fails, and when that fails there simply isn't a value to put in the cache at all (ie. wrong type in the CSS or you used a reference that doesn't exist). applyStyle isn't allowed to throw exception, so results are "best effort" (and how this worked is pre-existing, I didn't change it).
There was a problem hiding this comment.
My understanding is that if one control fails to applyStyle, the other control (which encountered no issues) should pick up the values from the stylesheet, and certainly not the default.
Or am I missing something?
There was a problem hiding this comment.
I must point out: applyStyle is not allowed to fail, so this is pretty much an edge case already (which I just exploit in the test to ensure the exceptional path is adding SKIP in exceptional cases).
The exceptions are supposed to come from lookup, which if it fails with an exception means that you simply do not know what the CSS value could be. This is then stored in the cache entry, and any sharing of that entry assumes the entry is created correctly (which it was if lookup was failing, as there is no retrying here as the CSS evaluation will just return the same exception).
So that it resets the value for other Nodes is expected, as there is no CSS value known as computation failed (artificially in this case, but failed nonetheless).
Again, in this specific case, I agree we could do better, but it seems beyond the scope of this fix (it has always worked like this -- an exception in applyStyle resulted in the system assuming that for that cache entry there is no CSS based value -- no matter how wrong that assumption is).
I can fix this as well if you want, but:
applyStyleis not allowed to throw exceptions, so what are we truly fixing here?- I didn't change this
- It has nothing to do with the problem that triggered the creation of this fix (which IMHO is already not something I caused... so I'm being nice here).
| * The cached value is still useful for others sharing this entry. | ||
| */ | ||
|
|
||
| if (!cssMetaData.isSettable(node)) continue; |
There was a problem hiding this comment.
the contract for CssMetaData.isSettable() says "This method is called before any styles are looked up for the given property." but here it's called after the lookup() in L929
There was a problem hiding this comment.
That's an odd amount of detail that is rather implementation specific.
I would suggest we remove it, as I don't think there are good reasons for this other than "that's how it works currently", and it actually is an active source of problems.
A property being unsettable does not mean you can't evaluate the stylesheet to find what value it would have gotten (and that's what we do now). The only reason this method exists is that is faster than catching "RuntimeException: A bound value cannot be set.".
There are two good reasons to still evaluate the CSS value (even if we won't set it):
-
The property may not be bound forever; if the cache entry was created without the calculation of the value for a "locked" property, then we later won't know what CSS based value to put in there (note: unbinding a property is not detected, so binding/unbinding already plays very badly with the CSS system).
-
The cache entry may be shared with siblings in the same state; if the cache entry was initialized by a Node with bound properties, and we then just skip those, then the other siblings would not have that value either -- this causes incorrect value resets and properties to not be styled, which this PR specifically wants to address
If there are good reasons to keep it, then the only other option we have is to NOT create a cache entry at all when a Node has any CSS property bound (a sibling that is in a better state may create it still though). This could be a performance hazard though, because if it is the only node of its kind, it would always need to go through the slow lookup path until there are no more bound properties.
There was a problem hiding this comment.
you mean removing the spec "This method is called before any styles are looked up for the given property." ?
There was a problem hiding this comment.
Yes, I don't think that should have been in the spec in the first place, as it reveals basically the inner workings of the CSS system. Also, we're still respecting the return value of isSettable -- we're not calling applyStyle (which would indeed result in the "expansion of the property" the documentation alludes to). We're merely looking up the styles, something the documentation specifically says it won't do -- but why guarantee that in the first place?
So I suggest rewriting the docs to:
* Check to see if the corresponding property on the given Node is
* settable. This method is called before any call to {@code applyStyle} is
* made. It is abstract so that the code can check if the property
* is settable without expanding the property. Generally, the property is
* settable if it is not null or is not bound.
Changing the sentence "This method is called before any styles are looked up for the given property." to "This method is called before any call to {@code applyStyle} is made." -- in both cases this ensures the property is not expanded unnecessarily, but gives the CSS system the freedom to still evaluate the stylesheet so it can create a shared cache entry that is complete.
|
Tested on multiple applications yesterday and today, all good. Also no performance issues or anything noticeable. |
Thanks Marius :) |
Maran23
left a comment
There was a problem hiding this comment.
Looks good. The new documentation is also welcome!
Will reapprove if we agree to change CssMetadata.isSettable here.
|
Agree with @Maran23 , this is an improvement and looks good. I would like to know your take on these two comments:
And, a question for @kevinrushforth : would the |
It might be possible, but it is hard to say how far that could go (CSS is quite complex). If you're looking to make things more testable, then I would start by splitting up For testing transitions, I'd probably not use JUnit for something like this but a more dedicated state transition test system (I've developed an in-house one) where one simply specifies actions (with preconditions), and assertions that must hold; the framework then figures out what combinations to test. Otherwise you either will make your own "loops" in JUnit, or find that using
I doubt it, it is an API not that much used, and the opposite approach is actually what was doing the backfiring. |
|
re: testing Just thinking out loud, we could make an attempt at exhaustive testing by first enumerating the possible scenarios, followed by iterating over combinations of those. For example: S01 - style change in owner and then iterate over 2^5 combinations of these, or probably less because some combinations might not be valid. This is just an example, I am sure there are many more possible scenarios, the main benefit I see is that we can test all the valid combinations. What do you think? |
I added some tests in #2225 regarding testing different child selectors and pseudoclasses. |
andy-goryachev-oracle
left a comment
There was a problem hiding this comment.
Question: should this change be mentioned in the release notes (along with other CSS improvements)?
I don't know, it is mostly a fix for a very old bug, that became a bit more visible later on. The CSS system hasn't changed. So if you list these kinds of bugs then I guess it could become part of the release notes. I've updated the |
There was a problem hiding this comment.
New CssMetaData doc change looks good.
We should wait for @kevinrushforth if this needs a CSR or not.
The doc change by itself seems reasonable, and I'd be OK doing it without a CSR. The only question I have then is regarding the behavioral change itself: would a "reasonable application" be surprised by the new behavior? If not, then we can skip the CSR. |
I don't think so, in fact, I don't think a reasonable application could even detect the difference without digging deep into CSS specifics -- the |
That's what I thought. I think this can go in without a CSR. |
|
@hjohn Then I think the last 'blocker' is the warning in the PR body: |
|
/integrate |
|
Going to push as commit 74e934a.
Your commit was automatically rebased without conflicts. |
Fix CssStyleHelper treating absent cache values as "no style applies"
I uncovered a problem that has been in the CSS engine for a long time, even before #1076 was applied. However, #1076 made this problem more obvious because of a side fix that was done there:
BitSetequalsimplementation was updated to NOT take the length of the allocated array (to store the bits in) into account for equality. As this array is just allocated on demand depending on what bits were set and reset, it should not be taken into account for equalityThe above bug hid problems when nodes were supposed to share a CSS cache entry, but didn't because their
BitSets were considered different (even though semantically, they were the same).With the fix in #1076, a lot more cases were sharing CSS cache entries (as they should) but this now exposed a bug in how
CssStyleHelperhandled the absence of a cached property. Basically, absence could mean two things before this change:CssMetaDatachanging!) or because the property was not settable (because it was bound)The CSS engine always assumed the latter, which means that if for whatever reason the cache entry was created by a Node that had outdated
CssMetaData(a Control that is yet to be skinned, or one where a CSS property was unsettable), the engine would assume that such a missing property was unstyled and can safely be reset. As entries are shared, this doesn't hold true for all nodes that share the same entry (if another node that shares the same entry has differentCssMetaDataor did not have the same property bound, then it may have been styled, and should not be reset!).Tests that confirm the problem
I added 4 new test cases, for four paths that could potentially result in the wrong things being in the cache:
applyStylestill results in aSKIPentry in the cacheAll of those tests should fail with the old implementation. The test that most closely resembles the problem with the regression reported in JDK-8388378 is the changing CSS meta data test (that simulates a skin replacement):
Controldidn't have a chance to run yet, meaning their CSS metadata does not have the property-fx-indentin them -- the CSS engine therefore doesn't know of that property and created a cache entry lacking it-fx-indentproperty was treated as "unstyled" and it got its value reset to whatever the default indent was...The Change
I adjusted
CssStyleHelperto only ever use explicitly set values in the cache; this can be a non-nullCalculatedValueor the special valueSKIP. If nothing was in the cache at all, it is interpreted as "never evaluated" and so must be looked up. It no longer conflates absence of a value with being unstyled.This however does mean that we must store more values in the cache. Any value that was evaluated, but did not result in a value that came from CSS is now set to
SKIP, as otherwise it would be absent and the new logic would then re-evaluate it each time.So what does that effectively mean? It means that where before a cached entry only contained entries that were explicitly assigned a CSS value, it now always contains all properties found in the CSS metadata -- most of those will be set to the sentinel
SKIP, so the cost for this is low. As the entry is also shared (and is now much safer to share) the total cost for these extraSKIPvalues in the cached entry is probably neglible, but it is certainly more than it was before.I think correctness here is more important than slightly more overhead, and since it was the only solution I could think of that would solve these problems, I went ahead with this fix.
If later it proves the overhead is not as neglible as I thought, we can adjust it knowing that we have several tests that catch problems with incorrect use of caches. One possible direction to go here is to use a more efficient
HashMapimplementation, possibly even just a simple list of values instead of a map, but that's definitely out of scope IMHO.Note
It's possible this may fix other hard to pin down and obscure CSS bugs, but I haven't done a search for possible other tickets that may be resolved by this fix.
Progress
Issue
Reviewers
Reviewing
Using
gitCheckout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/2218/head:pull/2218$ git checkout pull/2218Update a local copy of the PR:
$ git checkout pull/2218$ git pull https://git.openjdk.org/jfx.git pull/2218/headUsing Skara CLI tools
Checkout this PR locally:
$ git pr checkout 2218View PR using the GUI difftool:
$ git pr show -t 2218Using diff file
Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/2218.diff
Using Webrev
Link to Webrev Comment