View Issue Details

IDProjectCategoryView StatusLast Update
0000435NoesisGUIUnitypublic2018-11-21 13:47
Reporterai_enabled Assigned Tosfernandez  
PriorityhighSeveritymajor 
Status assignedResolutionopen 
Summary0000435: Circular references in GC with event handlers
Description

Hello!
As you know, our game now get high reception. Users noticed very little issues with the current release, but everybody noted us that game performance degrades over time: FPS drops very much after around 1 hour of playtime, and restore to normal only after re-launching game.
We also experiencing this issue all the time with NoesisGUI, but not pay much attention to it before. I'm also added some code to investigate this issue (the command to printing NoesisGUI current controls tree to a file - use console command "debug_noesis_tree"). But issue seems to be independent from controls count (it stays in range 11000-13000 during play and not increase over time, most high usage controls like "ItemInventoryIconControl" have reuse-pool (controls removed to manager after use and added later when required)).
With the Unity Profiler, I determined reason for FPS drop - invoking time of
NoesisGUIPanel.LateUpdate() (UIRenderer.Noesis_UpdateRenderer) increase over time, and after some time it's became very very high.
This issue seems to present on Mac & Linux also!
My idea - you can download our game and replace Noesis.dll with a debug version to investigate this issue.

Attached Files
SkillIconControl.xaml (17,138 bytes)
PlatformAny

Activities

ai_enabled

ai_enabled

2014-08-08 10:21

updater   ~0001487

Hello!
Do you have any progress with this issue? Is it reproducible on your machines?

sfernandez

sfernandez

2014-08-08 15:16

manager   ~0001489

Hi,

I've been doing some tests and after playing around 15-20 minutes the timings of our Update vary almost nothing. I've been killing some pirates, docking in the station, talking to the civilians, navigating through skill tree...

I will leave the game running during a few hours and see if it degrades then.

Is there anything special I should do to force the degradation more?

ai_enabled

ai_enabled

2014-08-08 16:10

updater   ~0001491

It's not degrading after something special, we're notice constant degradation over play time. Usually after 2 hours the game became unplayable because of a serious FPS drop as reported by many customers. Not sure if it will drop if you just leave your PC for a few hours.
Please try buying-selling items on space stations, especially in the "capital" star systems with big goods list (you can use "/tp <System_Name>" chat command to jump between star systems, and "/tp X Y" to jump inside current star system). Maybe we have some kind of leak with item icons controls or item tooltips - maybe something not disposed properly. But controls count in the Logical Tree stay at nearly constant over (active) playtime.
I found that the game have leak with assets (textures/sounds not unloading from cache, so game can eat up to ~1.5 Gb of RAM after visiting many different star systems), but this seems to not degrade performance.

sfernandez

sfernandez

2014-08-08 16:56

manager   ~0001492

I have a clue. Time is growing on the animation manager.
I will add some checks to see if the number of animations is growing constantly.

ai_enabled

ai_enabled

2014-08-12 03:08

updater   ~0001494

Hello! Do you have any progress? I can provide you game sources if you need it. This issue is critical for us...

sfernandez

sfernandez

2014-08-12 19:37

manager   ~0001496

Hi,

I detected that animated objects and properties are growing any time I enter the SKILLS tab. 66 new objects are added to the Animation manager and never deleted when leaving the tab.

Are you using "Forever" animations?
Are you keeping strong references to the controls used in the SKILLS tab so they are not destroyed?

That will explain why animations are never removed from the manager.

ai_enabled

ai_enabled

2014-08-13 09:09

updater   ~0001502

Last edited: 2014-08-13 09:14

Thanks for investigation!
First, about "Forever" animations - no, we don't use this type of animations. I attached SkillIconControl.xaml , please have a look on it and tell me if something wrong.

I've added some test code, and found that we have serious amount of controls, which are not referenced by anyone (which was simply removed from collection and expected to be garbage collected) and not automatically garbage collected (I tried forcing it).

Issue with skill icons is very strange. They're placed in specific panel control, which is placed via TabItem.RefreshContent() method to TabItem. So when I invoke TabItem.RefreshContent(null), this icons and they parent became abandon controls and should be collected by garbage collector, but this never happens.

Is this behaviour of abandoned controls normal and I should always remember to manually dispose all unused instances? I'm sure they can be disposed automatically, but some references still hold by someone. Is not easy to determine by who exactly - there is no such feature in Mono/.NET.

sfernandez

sfernandez

2014-08-13 16:50

manager   ~0001505

The SkillIconControl.xaml seems correct, nothing strange that could lead to this situation.

We recently discovered a big problem that could explain why objects are not released, but I don't know if it can apply to your scenario.

When you have a control, and in its code-behind you attach a delegate to an event of the control or any of the children elements, a strong reference to the control is stored by the C# delegate, so control is never disposed unless you remove the delegate.

For example, imagine a UserControl with a button inside:


public class MyControl : Noesis.UserControl
{
//...

public void OnPostInit()
{
    var btn = FindName<Noesis.Button>("btn");
    btn.Click += OnBtnClick;
}

private void OnBtnClick(Noesis.BaseComponent sender, Noesis.RoutedEventArgs e)
{
    // ...
}

// ...

}

This control will never be disposed, because Click event maintains a strong reference to the control.

We are very worried about this situation and solve it is one of our priorities.

Let me know if this is the problem you are experiencing.

ai_enabled

ai_enabled

2014-08-13 18:32

updater   ~0001506

I thought about it also and found that actually we're using event handlers in the skill icons control...
Usually we're following pattern "subscribe in OnLoaded, unsubscribe in OnUnloaded", but for some controls we didn't pay enough attention - because in WPF is not required: usually these event handlers creates only references to children controls, so whole hierarchy of control and its children can be cleaned by GC.
Do you have any ideas how to avoid this problem? Currently I can easily list all undisposed controls and fix code for each of them, but it doesn't solve any future problems if we again forget to remove references.

ai_enabled

ai_enabled

2014-08-15 12:11

updater   ~0001507

Last edited: 2014-08-15 12:12

Our base classes for controls using two events:

public void OnPostInit()
    {
        this.InitControl();

        this.Loaded += this.LoadedHandler;
        this.Unloaded += this.UnloadedHandler;
    }

But it's preventing garbage collection of any inherited control... but we absolutely can't live without it!
Any idea how we can resolve it?

sfernandez

sfernandez

2014-08-15 13:36

manager   ~0001508

I just come up with an idea that maybe will be the internal solution we are going to adopt.

The problem here is that event delegates (which maintain a strong reference to the object) are (and need to be) stored in a static table. So GC is unable to detect circular references and dispose the object when nobody else is referencing it.

So we need to break this strong reference somewhat. To do it we are going to introduce an indirection. We create an intermediate class that will hold a weak reference to the control object, and that class will be the one attached to the event:


[Noesis.Extended]
public class UserControlBase : Noesis.UserControl
{
public void OnPostInit()
{
this.InitControl();

    EventManager eventMgr = new EventManager(this);
    this.Loaded += eventMgr.OnLoaded;
    this.Unloaded += eventMgr.OnUnloaded;
}

protected virtual void InitControl() { }

internal virtual void LoadedHandler() { }
internal virtual void UnloadedHandler() { }

private class EventManager
{
    public EventManager(UserControlBase control)
    {
        target = new WeakReference(control);
    }

    public void OnLoaded(Noesis.BaseComponent sender, Noesis.RoutedEventArgs e)
    {
        UserControlBase control = target.Target as UserControlBase;
        if (control != null)
        {
            control.LoadedHandler();
        }
    }

    public void OnUnloaded(Noesis.BaseComponent sender, Noesis.RoutedEventArgs e)
    {
        UserControlBase control = target.Target as UserControlBase;
        if (control != null)
        {
            control.UnloadedHandler();
        }
    }

    private WeakReference target;
}

}

Does it make sense?

ai_enabled

ai_enabled

2014-08-15 17:22

updater   ~0001509

Last edited: 2014-08-15 17:23

Thanks! This solution works, but it's a little bit "boilerplate" solution and the EventManager instance will remains into memory if you don't hang down events explicitly. It's requires writing some extra code just to wrap the events correctly, but it's reasonable if there are no better solution.

Have you read this article about weak references? http://www.codeproject.com/Articles/29922/Weak-Events-in-C Maybe you can invent something better.

But the way, we just released new version of the game with fixed controls instantiating (and very high reusing of them). It still contains small "controls leak" due to OnLoaded/OnUnloaded hard references, but performance is much better now. Thanks for you help!

sfernandez

sfernandez

2014-08-15 18:46

manager   ~0001510

Thanks! This solution works, but it's a little bit "boilerplate" solution

Yes, this must be considered a temporal solution until we fix it internally.

and the EventManager instance will remains into memory if you don't hang down events explicitly.

But now is possible because your control is disposed, so in the UserControlBase (or whatever name you are using) destructor, you can unregister from Loaded/Unloaded events (you only need to keep a reference to the EventManager created in the OnPostInit).

Have you read this article about weak references? http://www.codeproject.com/Articles/29922/Weak-Events-in-C [^] Maybe you can invent something better.

This is totally the idea, we will expose Weak Events instead, so you don't have to worry about anything.

ai_enabled

ai_enabled

2016-01-04 11:59

updater   ~0003427

Last edited: 2016-01-04 11:59

Hi guys,

I just want to check if you will improve this with v1.3 or we will need to keep the workaround?

Thanks!

Issue History

Date Modified Username Field Change
2014-08-02 03:34 ai_enabled New Issue
2014-08-05 14:01 sfernandez Assigned To => sfernandez
2014-08-05 14:01 sfernandez Status new => assigned
2014-08-08 10:21 ai_enabled Note Added: 0001487
2014-08-08 15:16 sfernandez Note Added: 0001489
2014-08-08 15:16 sfernandez Status assigned => feedback
2014-08-08 16:10 ai_enabled Note Added: 0001491
2014-08-08 16:10 ai_enabled Status feedback => assigned
2014-08-08 16:56 sfernandez Note Added: 0001492
2014-08-08 16:56 sfernandez Status assigned => feedback
2014-08-12 03:08 ai_enabled Note Added: 0001494
2014-08-12 03:08 ai_enabled Status feedback => assigned
2014-08-12 19:37 sfernandez Note Added: 0001496
2014-08-12 19:37 sfernandez Status assigned => feedback
2014-08-13 09:09 ai_enabled Note Added: 0001502
2014-08-13 09:09 ai_enabled Status feedback => assigned
2014-08-13 09:09 ai_enabled File Added: SkillIconControl.xaml
2014-08-13 09:11 ai_enabled Note Edited: 0001502
2014-08-13 09:11 ai_enabled Note Edited: 0001502
2014-08-13 09:14 ai_enabled Note Edited: 0001502
2014-08-13 09:14 ai_enabled Note Edited: 0001502
2014-08-13 16:50 sfernandez Note Added: 0001505
2014-08-13 16:50 sfernandez Status assigned => feedback
2014-08-13 18:32 ai_enabled Note Added: 0001506
2014-08-13 18:32 ai_enabled Status feedback => assigned
2014-08-15 12:11 ai_enabled Note Added: 0001507
2014-08-15 12:12 ai_enabled Note Edited: 0001507
2014-08-15 13:36 sfernandez Note Added: 0001508
2014-08-15 13:36 sfernandez Status assigned => feedback
2014-08-15 17:22 ai_enabled Note Added: 0001509
2014-08-15 17:22 ai_enabled Status feedback => assigned
2014-08-15 17:23 ai_enabled Note Edited: 0001509
2014-08-15 17:23 ai_enabled Note Edited: 0001509
2014-08-15 18:46 sfernandez Note Added: 0001510
2014-08-15 18:46 sfernandez Status assigned => feedback
2015-07-23 02:38 jsantos Category Unity Package => Unity3D
2015-10-08 20:03 jsantos Summary Performance degrade greatly over time => Circular references in GC with event handlers
2016-01-04 11:59 ai_enabled Note Added: 0003427
2016-01-04 11:59 ai_enabled Status feedback => assigned
2016-01-04 11:59 ai_enabled Note Edited: 0003427
2018-11-01 02:14 jsantos View Status public => private
2018-11-21 13:47 jsantos View Status private => public
2018-11-21 13:47 jsantos Platform => Any
2025-10-10 13:29 jsantos Category Unity3D => Unity