-
Notifications
You must be signed in to change notification settings - Fork 261
Remaining Memory leaks and application crash at more than 2000 tree entries #1001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
We should focus in the crash first, the mem leaks are most like only a consequence of the crash. But I don't see a crash here with latest Virtual TreeView source code and Delphi 10.4.1. I opened the project, pressed F9, pressed "Fill VT", double clicked the VT control. The application terminated without errors. Can you send the call stack and exception type of the crash please? |
We have same problem. Delphi Version: 10.3 Update 1 Stack from another programm
|
I just committed a patch, could you please check if it solves the problem for you? |
Hi Joachim, Unfortunately the patch doesn't help. The problem still exists... Henrik |
Hi, out of interest, I had a look at the demo project too and could reproduce the crashes with FastMM4 and FullDebugMode. From my understanding, it looks like I was able to fix this by adding a call to Let me know what you think. Sebastian |
It could not, because the code passed to Synchronize is executed asynchronusly, and if it happens when the tree has already been destroyed, Self becomes invalid and both FStates and HandleAllocated will fail, or even worse: you might got 'lucky' so that the memory Self points at will still be available, HandleAllocated and FStates will still contain some valid-looking values, the check will pass, but executing DoStateChange will fail, because object's memory will have been overwritten already by that time, with VMT reference leading to some random memory. To solve this, you must ensure that no code like this is scheduled to run or already running at the time TBaseVirtualTree.Destroy executes |
@VladimirBartosh: hence my suggestion of adding CheckSynchronize - that should ensure that the asynchronously called code is executed before the destructor continues. |
CheckSynchronize can also be not enough. Imagine this:
|
Thanks for your input @VladimirBartosh - that is why I had my doubts about CheckSynchronize too. The more I look at the code, the more it feels like the local If I'm not mistaken that was introduced in #844 - so hopefully @joachimmarder can shed some light on why this was done. |
Unfortunately Delphi lacks the concept of a cancellation token:, which would help here.
lCurrentTree was already there in the initial commit of the file in 2015. I don't see a connection to #844 here.
Sorry, but I don't see why. lCurrentTree is assigned to FCurrentTree at the moment this tree gets processed. And even if you make the assignment of nil the last line, the problem reported here persists. |
* TWorkerThread.ReleaseThreadReference() calls CheckSynchronize() to Make sure code queued in the main thread by TBaseVirtualTree.ChangeTreeStatesAsync() get processed before the tree is being destroyed. * Removed unused property CurrentTree from TWorkerThread.
Using |
@joachimmarder, yes, you're right - I'm trying to understand the intent the original code had. And The change in commit 0838277 as part of #844 is quite severe. Before that I can fix the crash of the test-case by @antsiparovich by changing the following things:
Wouldn't you agree that this solution is easier to understand than your CheckSynchronize call in TWorkerThread.ReleaseThreadReference? In fact, if this is done, I think lCurrentTree can be removed completely, which would further simplify the code. What do you think about adding this test-case to a Test project with DUnit or DUnitX? Sure it's not really a unit-test and you wouldn't be able to run these kind of tests from the command line, but I think it could help to avoid regressions in the future. Last but not least: I don't understand why |
I was wondering why the test project from @antsiparovich was hanging on shutdown when used with FastMM and it turns out it wasn't hanging, but actually trying to report a leak if 10k UnicodeStrings. The following code needs to be added to Unit3 to fix this:
|
That may be possible. But I still think it is necessary to prevent the code to be executed in case the control is being destroyed. In a quick test with the project above I got AVs when I simply commented this line. We could of course something |
* TWorkerThread.ReleaseThreadReference() calls CheckSynchronize() to Make sure code queued in the main thread by TBaseVirtualTree.ChangeTreeStatesAsync() get processed before the tree is being destroyed. * Removed unused property CurrentTree from TWorkerThread.
The thing is, with that condition being left there, my fix of getting rid of Your fix OTOH is quite fragile - it calls CheckSynchronize only once - and there's absolutely no guarantee that it will finish all synchronized code with that Tree.
There is no way to find out if a control already has been destroyed, so it doesn't help much if you can find out if it's just in the process of being destroyed. I'm pretty sure the reason why the And it's perfectly fine for a tree to not have a handle nor a parent window. Imagine having a DropDown component with a tree which you do all sorts of things with, before the user eventually drops down the control and the tree gets a parent, a window handle and finally becomes visible. As the code is now, I agree with @VladimirBartosh: there are quite a few circumstances where the current WorkerThread code can possibly crash and IMHO the only way forward is to add test-cases, so you can gain a bit of confidence that new changes don't break things. I've started a simple test-case based on the test project here and as soon as the worker thread is involved, crashes start to occur (because I'm repeating the test 100 times). The goal now is to get this running reliably. For anyone interested, I've checked in just the test-case on the develop/thread-tests branch of my fork (link below my comment). |
This is not correct, it is important to ensure that no new method is queued in the main thread for this control if it is being destroyed, because CheckSynchronize is called in the context of the destructor.
I don't think calling it multiple times helps here. It is important to call it at the right moment (the destructor) and to make sure that no new methods are queued for this control afterwards.
OK, I will change it to |
Thanks for creating this. I added it to the official repository and ran the tests project several times, the tests always passed. Is your fork maybe missing some changes? Or has additional changes? |
Unfortunately that's not the reason you're not seeing the crashes. In the mean-time I found out that for me, it only crashes on Win32 in the debugger. It does not crash when run without the debugger and it does not crash with Win64, regardless of the debugger. I was really puzzled by this and even started considering a bug in the debugger, but what I found very odd is that I see the exact same behavior both with RX 10.1 Berlin and RX 10.4 Sydney. And as soon as I remove the SetChildCount, i.e. avoid that any WorkerThread is started, the crashes go away with Win32 too. So it seems I was bit too optimistic about adding reliable test-cases, but I have not given up yet. I'll definitely also check the good old DUnit GUI Runner and if all else fails will actually create and display the forms from the test case here. |
The bigger question I wanted to ask is whether this threaded validation is worth the trouble.
|
Honestly, I didn't even consider this possibility, also because I think the chances are very high that Embarcadero will just consider this "as designed". If you want to take the chances and report it, I would highly recommend including a solid test-case, but even then, my hopes aren't high. Also I'd refrain from suggesting how this should be fixed and leave it up to them, but that is just my personal experience with bug reporting. |
In
This is not about absolute objective performance, but about subjective performance for the user. If you manipulate a large tree while positioned at the top, there is little that needs to be done to be able to update the GUI and let normal operation continue. The positional cache is necessary to speed up things like vertical scrolling and hit-testing, which gets even more complicated, if variable node height is involved. Imagine a tree with a million visible nodes and you're positioned at the very end. Just adding a single node makes it necessary to iterate through all of them to determine the exact position of the last node. If you'd do this synchronously, the tree would feel very sluggish. If you want to see this in action, have a look at the Advanced Demo and enable state tracking in the secondary window (this is also a demonstration why state change events need to be synchronized).
Even if we could, that doesn't solve the problem. The validation of the position cache is walking the tree nodes in a separate thread. It has to be aborted before any changes are made to the nodes, otherwise crashes and undetermined behavior are guaranteed to happen, even if not always reproducible. |
…deadlock in case CheckSynchronize() is called nested.
* Partial fix for issue #1013: Changing the Check State from InitNode may leave the tree in "updating" state.
…onized calls from ChangeTreeStatesAsync() in the queue.
Just to avoid a misunderstanding here: This does not mean the nodes visible in the current viewport, but all nodes that are not in collapsed branches. So it's not unlikely.
That's unfortunately not sufficient, as @modersohn wrote earlier. And I also agree with him that this code makes sense. I don't think it would be an overall benefit to remove this code. For what it's worth: With the latest code I am unable to replicate any of the AVs or mem leaks reported above. |
Yes, but the fixes feel very circumventional (i.e. 2 calls to But I understand you need a workable solution right now - for a long term solution, I'd like to invite you all to post some feedback to the proposed thread-less alternative I described in #1014 |
The catch22 is that the WorkerThread is using Synchronize indirectly in two places:
This is why InterruptValidation, called from the main thread has to use CheckSynchronize and the issues that this has. The solution is to avoid using Synchronize. Then, InterruptValidation can do a standard wait using some simple synchronization mechanism. ChangeTreeStatesAsync can be changed to not use Synhronize as indicated above. It can use the suggested lock-free mechanism and UpdateEditBounds is not an issue since it does nothing when called from a thread different than main. So the only problem is MeasureItemHeight . I have not looked into this, but you use the WorkerThread only for trees with uniform height. If you agree that this is the right direction and I can submit a PR. |
Your idea certainly has merit, but the task of completely avoiding
While your proposed code would be able to change
The fact that
One could certainly disable asynchronous validation when Let's see if and how we can work out the remaining issues. |
Oh and there's one case for |
You can always shut the thread down raise the Exception and start a new WorkerThread when needed, or use Queue instead of synchronize. In fact this is another change I want to see implemented: Use queue instead of synchronize in the WorkerThread exception handling. |
Who would shut it down and raise the exception? Imagine the thread already has a list of several trees waiting for validation - how would you be able to manage all of this? I can't see how.
Again, that would not solve the synchronization problems, I think. The exception is acquired to a local variable in |
In this case the list of trees would be stored inside a global thread-safe list. The Terminate event could schedule the start of a new worker thread.
This is if, as now, the WorkerThread stays around till shutdown.
This however is a problem but may be acceptable since the only states that ChangeTreeStatesAsync changes have to do with validation and could be considered as internal and of no interest to the user. You also could use a different enumeration TValidationState or TCacheState, that is stored in a separate field instead of including them in State. |
The The exception handling with Queue might actually work if the compiler is smart enough to capture the exception correctly even when the thread has already been freed. I'm going to try that later with a test-case and memory leak detection enabled.
That is certainly something to consider, but it does change the current behavior in quite a substantial way - since you never know who uses the OnStateChange event for whatever reason. Therefore renaming and moving the state to a separate enum is certainly the preferable approach as it would intentionally break any code out there that does work with these states. |
But there is no need to wait for that. You could instead start a new FreeOnTerminate WorkerThread before exiting the existing one.
You could instead store the Exception in a private field of the Tree. and raise it in the queued anonymous method. It is important to avoid using synchronize. |
I would like to close this issue in case none of the above issues can be reproduced anymore. I don't claim that this is a good solution, but it is at least some solution. Please continue discussions about alternative designs in issue #1014. |
I'm sorry to be the bringer of bad news, but I found about this issue because we are facing its exact consequences, despite having updated to today's git content.
Our fix is to change it to this:
First, we discovered that our our VTV instance was in the With this, the issue is fixed for good. |
Are you sure that the
Yes, this makes a lot of sense, and I am committing this code change now. One idea would be to do this as long as the |
At the time the destructor is called, Note that the blocked call to
So, at the time the destructor is called, Note that out of |
…troy() is not enough, the TWorkerThread may be stuck in the first call to ChangeTreeStatesAsync() where tsValidating is not set.
OK, yes, although it seems unlikely, the |
Hi.
When there are more than 2000 entries in the Virtual-TreeView and the window is closed on double click then the application crashes with memory leaks.
FastMM memory leaks:
5 - 12 bytes: TObject x 2
13 - 20 bytes: TList x 1, TThreadList x 1
21 - 28 bytes: Unbekannt x 1
37 - 44 bytes: TBaseVirtualTree.ChangeTreeStatesAsync$ActRec x 1
53 - 60 bytes: TWorkerThread x 1
Delphi Version: 10.2 (also in 10.4)
Virtual Tree View Version: 7.4
A small demo program is attached: VTBug.zip.
My assumption is that the TWorkerThread is performing something with the internal cache while the tree is destroyed.
The application doesn't crash at less than 2001 tree entries or when an Application.ProcessMessages is performed before the tree will be destroyed.
The text was updated successfully, but these errors were encountered: