On Wed, 10 Jan 2024 23:20:23 GMT, John Hendrikx <jhendr...@openjdk.org> wrote:

>> thank you for clarification!  
>> 
>> perhaps we ought to add a comment explaining why: it's one thing to create a 
>> single chain of conditions linked to a property, but here we keep creating 
>> new property instance, so the side effects are not that obvious.
>
>> I noticed however that it is not as simple as that, and the current PR 
>> introduces regression, as the SystemMenuBarTest.testMemoryLeak test is now 
>> failing. Listeners are not only registered when setMenus is called, but also 
>> when menu(Item)s are added to menus. I'm not sure if a single active 
>> property can address this, as we don't want to remove all listeners for all 
>> menuitems in case a particular menuitem is added to a particular menu.
> @hjohn does that make sense?
> 
> So, if I understood correctly, that test is dealing not with the entire menu 
> bar disappearing (for which the `active` property ensures clean up), but with 
> a submenu getting cleared, and then a new item added.  Any old items that 
> were added should be garbage collectable after clearing.
> 
> This is a bit tricky.  Basically I think it means that the listeners on a 
> certain `MenuBase` wrapper should be removed in two cases:
> 
> 1. When the entire menu is no longer needed (what we have now)
> 2. When the `MenuBase` in question is removed from its parent
> 
> I think this may be resolved in one of two ways:
> 
> 1. Create a specific active condition for `when` that includes a check on 
> parent and on the top level `active` property
> 2. Gather all listeners added into a single `Subscription` (currently the 
> `Subscription` that is returned is discarded, track this somewhere (probably 
> easiest in a the user property map) taking care not to create a new hard 
> reference (not sure if that will be an issue).  The `Subscription` can then 
> be obtained and unsubscribed early.
> 
> Now I think option 1 can be achieved by doing something like the code below.
> 
> **Note:** completely untested, and did not check yet if we aren't creating 
> any new problematic hard references; I can test this out over the weekend if 
> you like.
> 
>      // given the global `active` property...
>      // and a MenuBase mb, for which we can discover if it is a root menu or 
> not...
>      // and which exposes a new property hasParentProperty (you can derive 
> this property
>      // if you like by doing `parentMenuProperty.map(Objects::nonNull)`)
> 
>      BooleanProperty forWhen = active;
> 
>      if (mb "is not a root menu") {  // important we don't do this for root
>            // include "and" condition that it also must have a parent to be 
> considered "active"
>            forWen = active.flatMap(b -> b ? mb.hasParentProperty() : active);
>      }
> 
>      // then use `forWhen` in your `when` conditions instead of `active` as 
> before.
> 
> What the above does is that using `flatMap` it creates a l...

> perhaps we ought to add a comment explaining why: it's one thing to create a 
> single chain of conditions linked to a property, but here we keep creating 
> new property instance, so the side effects are not that obvious.

Yeah, the replacing of the property used in `when` is not a normal scenario.  
Normally, the property used in the `when` goes out of scope at the same time as 
the rest of the references, in cases like:

       label.textProperty().when(userInterfaceIsShowing).addListener( ... );

The `userInterfaceIsShowing` is then part of a `Node` based container, or is 
not referenced anywhere at all (it's just derived from say 
`node.sceneProperty().flatMap(Scene::windowProperty).flatMap(Window::isShowingProperty)`.

It is important to realize that `when` uses two (normal) listeners:
- One on its condition property (`active`), which is not weak and is **never** 
removed (iirc, it can't be weak as otherwise the observable created by `when` 
could get GC'd when the condition is `false`, but shouldn't have been as the 
condition may yet become `true` again).
- One on its parent property (x in `x.when( ... )`) -- this listener (and thus 
its hard reference) is only present when the condition evaluates to `true`

In this specific case there is nothing that tells us that the menu disappeared, 
so the best we can do is that when a **new** menu is created that at that time 
we discard the old one.  To be able to discard it, we unfortunately need to 
keep a hard reference to the `active` property we used (so it can be set to 
`false`).  However, the `when` has a listener on that `active` property (as it 
needs to know when the condition changes), and a listener points back to the 
observable value that `when` created (it is not, and can't be, a weak 
listener).  So in order to also cut that hard reference, we need to replace the 
`active` property as well after setting it to `false` (if you don't set it to 
`false`, there will be a listener on the parent of `when` that keeps a hard 
reference still... setting it to `false` removes that listener first).

So definitely not your run-of-the-mill `when` example, but it will at least 
always behave deterministically as there are no weak listeners used anywhere.  
If there is a bug, it will be easy to reproduce on someone else's machine.

The illustration I added earlier above should hopefully make the above a bit 
more clear, but of course some documentation in the code to explain this may be 
good too.

-------------

PR Review Comment: https://git.openjdk.org/jfx/pull/1283#discussion_r1448115136

Reply via email to