Skip to content

8386663: Stylesheet/StyleClass list should be lazily initialized - #2191

Open
Maran23 wants to merge 6 commits into
openjdk:masterfrom
Maran23:8386663-Stylesheet/StyleClass-list-should-be-lazily-initialized
Open

8386663: Stylesheet/StyleClass list should be lazily initialized#2191
Maran23 wants to merge 6 commits into
openjdk:masterfrom
Maran23:8386663-Stylesheet/StyleClass-list-should-be-lazily-initialized

Conversation

@Maran23

@Maran23 Maran23 commented Jun 17, 2026

Copy link
Copy Markdown
Member

This PR is an optimization for Node.styleClass and Parent.stylesheets.
Instead of always initializing both properties with an empty list, we are creating (therefore allocating) the list on the first access instead.
Similar to many other lazy properties.

Why?

  • Parent.getStylesheets() is very rarely used by developers and JavaFX code. So this list is very often completely unused and empty. Most developers usually only add stylesheets at the Scene
  • Node.getStyleClass() is usually not used (empty) for layout containers such as Pane or Group or shapes. A JavaFX App usually consists of a good amount of such containers (or shapes)

So that our CSS code is not initializing both lists on access, I added related NodeHelper and ParentHelper methods to return null when both lists were not initialized and therefore used. Otherwise we return the list as before.
This will save us some memory and allocation, which is both good for the memory consumption but also for Node / Parent creation (time).

Added documentation and tests. Will do some measurements with some apps very soon and attach it here.


Benchmarks

I wrote a very small scene graph analyzer snippet to measure the memory gain.
Feel free to test this on your own apps!

  1. Get the SceneGraphAnalyzer here: SceneGraphAnalyzer
  2. Add this code to your Scene and press the shortcut F12 when all of the UI is loaded:
        scene.setOnKeyPressed(event -> {
            if (event.getCode() == KeyCode.F12) {
                var analyzer = new SceneGraphAnalyzer();
                var res = analyzer.analyze(scene);
                res.print();
            }
        });
  1. If you have a modular app, add the following VM argument: --add-opens javafx.graphics/javafx.scene=yourapp

Projects

  1. Tested with JFXCentral
╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:                 654                ║
║    ├─ Parent nodes:           388                ║
║    └─ Leaf nodes:             266                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:         19 /  654  (  2.9%)    ║
║  Null stylesheets:       386 /  388  ( 99.5%)    ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):        1.34 KB              ║
║  Saved (stylesheets):      27.14 KB              ║
║  Saved (total):            28.48 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝

1.1. Test with an Icon Library at JFXCentral

╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:               1,032                ║
║    ├─ Parent nodes:           640                ║
║    └─ Leaf nodes:             392                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:         17 / 1,032 (  1.6%)    ║
║  Null stylesheets:       639 /  640  ( 99.8%)    ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):        1.20 KB              ║
║  Saved (stylesheets):      44.93 KB              ║
║  Saved (total):            46.13 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝
  1. Tested with XPipe
╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:               3,535                ║
║    ├─ Parent nodes:         2,733                ║
║    └─ Leaf nodes:             802                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:        656 / 3,535  ( 18.6%)   ║
║  Null stylesheets:     2,732 / 2,733  (100.0%)   ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):       46.13 KB              ║
║  Saved (stylesheets):     192.09 KB              ║
║  Saved (total):           238.22 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝
  1. Tested with JabRef
╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:               3,353                ║
║    ├─ Parent nodes:         2,379                ║
║    └─ Leaf nodes:             974                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:        444 / 3,353  ( 13.2%)   ║
║  Null stylesheets:     2,372 / 2,379  ( 99.7%)   ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):       31.22 KB              ║
║  Saved (stylesheets):     166.78 KB              ║
║  Saved (total):           198.00 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝
  1. Table heavy app
╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:                 747                ║
║    ├─ Parent nodes:           431                ║
║    └─ Leaf nodes:             316                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:         35 /  747  (  4,7%)    ║
║  Null stylesheets:       431 /  431  (100,0%)    ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):        2,46 KB              ║
║  Saved (stylesheets):      30,30 KB              ║
║  Saved (total):            32,77 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝
  1. Chart heavy app
╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:                 566                ║
║    ├─ Parent nodes:           345                ║
║    └─ Leaf nodes:             221                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:        146 /  566  ( 25,8%)    ║
║  Null stylesheets:       341 /  345  ( 98,8%)    ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):       10,27 KB              ║
║  Saved (stylesheets):      23,98 KB              ║
║  Saved (total):            34,24 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8386663: Stylesheet/StyleClass list should be lazily initialized (Enhancement - P4)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/2191/head:pull/2191
$ git checkout pull/2191

Update a local copy of the PR:
$ git checkout pull/2191
$ git pull https://git.openjdk.org/jfx.git pull/2191/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 2191

View PR using the GUI difftool:
$ git pr show -t 2191

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/2191.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper

bridgekeeper Bot commented Jun 17, 2026

Copy link
Copy Markdown

👋 Welcome back mhanl! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk

openjdk Bot commented Jun 17, 2026

Copy link
Copy Markdown

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk Bot added the rfr Ready for review label Jun 17, 2026
@openjdk

openjdk Bot commented Jun 17, 2026

Copy link
Copy Markdown

The total number of required reviews for this PR has been set to 2 based on the presence of this label: rfr. This can be overridden with the /reviewers command.

@mlbridge

mlbridge Bot commented Jun 17, 2026

Copy link
Copy Markdown

Webrevs

@Maran23

Maran23 commented Jun 17, 2026

Copy link
Copy Markdown
Member Author

Today I learned about https://github.com/openjdk/jol to measure the exact memory footprint of objects and used those insights to measure the memory gain of some apps - results in the PR description.

Memory stats:

com.sun.javafx.collections.ObservableListWrapper@76bbca23d footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1        16        16   [Ljava.lang.Object;
         1        40        40   com.sun.javafx.collections.ObservableListWrapper
         1        24        24   java.util.ArrayList
         3                  80   (total)

With -XX:+UseCompactObjectHeaders (always enabled in JDK-27, so I used this value as baseline):

com.sun.javafx.collections.ObservableListWrapper@7ffb7d94d footprint:
     COUNT       AVG       SUM   DESCRIPTION
         1        16        16   [Ljava.lang.Object;
         1        32        32   com.sun.javafx.collections.ObservableListWrapper
         1        24        24   java.util.ArrayList
         3                  72   (total)

Comment thread modules/javafx.graphics/src/main/java/javafx/scene/Node.java Outdated
@hjohn

hjohn commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator
  • Node.getStyleClass() is usually not used (empty) for layout containers such as Pane or Group or shapes. A JavaFX App usually consists of a good amount of such containers (or shapes)

The stats you showed lead me to a different conclusion: style class is usually used, and most things do have styles associated with them, including containers; most containers I create definitely have a style, they just don't have one by default.

I fully agree on the StyleSheets property.

@Maran23

Maran23 commented Jun 18, 2026

Copy link
Copy Markdown
Member Author

The stats you showed lead me to a different conclusion: style class is usually used, and most things do have styles associated with them, including containers; most containers I create definitely have a style, they just don't have one by default.

Yes. It might still be a good idea though, but I agree that it seems to have the biggest advantage for Chart/Shape heavy apps from what I measured.

It would be interesting to also measure an app that uses many Shape3D instances. As those have no styleClass as well by default. So I think those applications should also benefit from this. But I unfortunately don't have any myself, maybe I will check the Jfx3dViewerApp.

I still plan to test with some more application, but currently not at home so need to setup some of the projects on my laptop first.

@nlisker

nlisker commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

It would be interesting to also measure an app that uses many Shape3D instances. As those have no styleClass as well by default. So I think those applications should also benefit from this. But I unfortunately don't have any myself, maybe I will check the Jfx3dViewerApp.

There's https://github.com/openjdk/jfx/tree/master/tests/performance/3DLighting.

@drmarmac

Copy link
Copy Markdown
Contributor

I tested this in an app with lots of 2D shapes:

╔══════════════════════════════════════════════════╗
║ Scene Graph Analysis                             ║
╠══════════════════════════════════════════════════╣
║  Total nodes:               6,836                ║
║    ├─ Parent nodes:         4,850                ║
║    └─ Leaf nodes:           1,986                ║
╠══════════════════════════════════════════════════╣
║  Null styleClass:      2,048 / 6,836  ( 30.0%)   ║
║  Null stylesheets:     4,845 / 4,850  ( 99.9%)   ║
╠══════════════════════════════════════════════════╣
║  Saved (styleClass):      144.00 KB              ║
║  Saved (stylesheets):     340.66 KB              ║
║  Saved (total):           484.66 KB              ║
║                   (at 72 bytes / empty list)     ║
╚══════════════════════════════════════════════════╝

Nodes with null styleClass are indeed mainly containers and Shapes.
I'd say it's worth doing both optimizations.

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

I think this effort is similar to the idea of stashing rarely used attributes into a hidden hashmap.

@Maran23 would you like to cooperate (review, etc.) with me on this? Once we get through with the rare properties, Node, Shapes, and possibly other classes, we might see substantial improvement in RAM consumption.

@andy-goryachev-oracle andy-goryachev-oracle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it.

*/

if (styleClassNames.size() < selectorStyleClassNames.size()) {
if (styleClassNames == null || styleClassNames.size() < selectorStyleClassNames.size()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make more sense, instead of adding the null check here, move it up to after L278, to avoid unnecessary check?

@Maran23 Maran23 Jun 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will not really matter.
Just made the change locally to check what looks/feels better and personally it felt better in matchesStyleClasses.

Because it bascially reads like "well, a null styleClass will not match anything, so just return". But just my personally feeling here really.

Comment thread modules/javafx.graphics/src/main/java/javafx/scene/Node.java Outdated
@Maran23

Maran23 commented Jun 20, 2026

Copy link
Copy Markdown
Member Author

I think this effort is similar to the idea of stashing rarely used attributes into a hidden hashmap.

@Maran23 would you like to cooperate (review, etc.) with me on this? Once we get through with the rare properties, Node, Shapes, and possibly other classes, we might see substantial improvement in RAM consumption.

Yes. I think we could probably reuse and adjust the SceneGraphAnalyzer approach for some of the measurements we should do in the future for this one.

@Maran23

Maran23 commented Jun 20, 2026

Copy link
Copy Markdown
Member Author

Added two more Open Source projects to the benchmark section.

Maran23 added 3 commits June 21, 2026 16:49
TwoLevelFocusListBehavior was very weirdly relying on the toString() behavior
…Stylesheet/StyleClass-list-should-be-lazily-initialized
@Maran23

Maran23 commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

Seems like the latest GHA catched a race condition:

TaskEventTest > onCancelledCalledWhenSetViaProperty() FAILED
    org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
        at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
        at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
        at app//org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
        at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
        at app//org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:31)
        at app//org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:183)
        at app//test.javafx.concurrent.TaskEventTest.onCancelledCalledWhenSetViaProperty(TaskEventTest.java:375)

@andy-goryachev-oracle

Copy link
Copy Markdown
Contributor

Seems like the latest GHA catched a race condition:

It's a known issue: https://bugs.openjdk.org/browse/JDK-8357459

@andy-goryachev-oracle andy-goryachev-oracle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document the toString() format change in the JBS ticket and this PR description.

I don't know whether a CSR is needed, since the actual format has not been documented, but the fact that multiple tests fail with this change indicate that the applications that rely on the exact string (and not on .contains()) will break.

Comment thread modules/javafx.graphics/src/test/java/test/javafx/scene/ParentTest.java Outdated
@Maran23

Maran23 commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

I don't know whether a CSR is needed, since the actual format has not been documented, but the fact that multiple tests fail with this change indicate that the applications that rely on the exact string (and not on .contains()) will break.

But note that the tests did check for whole String. If we would change e.g. Node.toString(), then tests will also fail. If we would change a toString() method from any other list (e.g. getChildren), then there might be a test that checked the excact pattern - but not because it is needed but because we have tests to even check the whole String.

A normal contains on the list will work as before, even a contains on the String will work as before. Just that the whole sequence looks like one would expect from a list. I can't see a reason why application would use the value from toString other than debugging

@andy-goryachev-oracle andy-goryachev-oracle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioning this change in the toString() behavior in the JBS ticket should be sufficient, I think.

@kevinrushforth

Copy link
Copy Markdown
Member

Mentioning this change in the toString() behavior in the JBS ticket should be sufficient, I think.

I agree. No need for a CSR (nor a release note).

Comment thread modules/javafx.graphics/src/main/java/javafx/scene/Node.java Outdated
Comment thread modules/javafx.graphics/src/main/java/javafx/scene/Parent.java Outdated
* @param node the node
* @return the styleClass or null
*/
public static List<String> getStyleClassOrNull(Node node) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most (all?) helper classes that implement the private accessor pattern don't have documentation, because it is implied that it's a simple forwarding method.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was to explain why this method exists. Do you have a suggestion, or should we just remove it entirely?

@kevinrushforth

Copy link
Copy Markdown
Member

It doesn't look like this will be ready before the jfx27 RDP1 fork, but in the unlikely event that it is, wait until after the fork. Any CSS lazy eval fix has a relatively high risk of regression, so this is the sort of change that is better earlier in the release cycle.

@Maran23

Maran23 commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

It doesn't look like this will be ready before the jfx27 RDP1 fork, but in the unlikely event that it is, wait until after the fork. Any CSS lazy eval fix has a relatively high risk of regression, so this is the sort of change that is better earlier in the release cycle.

That would be my suggestion, too.
I also think that's the advantage of the release train model. Either it fits in on time, or it doesn't (which is not a bad thing).
And usually there is no rush.

(On that, I wouldn't want to merge #1970, #2175 or #2167 right before RDP1 either)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rfr Ready for review

Development

Successfully merging this pull request may close these issues.

7 participants