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
Improved graph rendering performance by reducing locks in settingscache #5540
Improved graph rendering performance by reducing locks in settingscache #5540
Conversation
Codecov Report
@@ Coverage Diff @@
## master #5540 +/- ##
=========================================
Coverage ? 35.22%
=========================================
Files ? 607
Lines ? 46294
Branches ? 6312
=========================================
Hits ? 16307
Misses ? 29236
Partials ? 751 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replacing locks with concurrent dictionary does not guarantee cache objects data consistency. It is like updating a database without transactions. My proposal to fix this is to cache the needed settings locally in places where they are fetched intensively or under another lock. Sth along the lines:
private bool _showRevisionGridGraphColumn;
public RevisionDataGridView()
{
AppSettings.OnChanged += (s, e) =>
{
_showRevisionGridGraphColumn = AppSettings.ShowRevisionGridGraphColumn;
};
...
@jbialobr The change you propose is very difficult to do and maintain. The settings are read all over the place. For example in functions like GetJunctionColor(). How about this; keep the concurrentdictionary. It makes it safe to read when other threads are writing. Only do the locks around the Set, Safe and Readfrom file. No locks around GetValue. This ensures the cache data is consistent. If you receive an old value from GetValue, I consider this a timing issue, not a concurrency issue. The GetValue is like ReadUncommitted...:) |
Sounds appealing, but I am still uneasy about it. There is a mechanism that refreshes the in-memory settings if the corresponding settings file is changed. Now before each GetValue GE ensures that we are in sync with the file. We would have to give up that and move the refreshing into a backgroud thread. Then when reading a settings group, like build server configuration, could be divided by the refreshing in the background into two parts of possibly inconsistent data. I know it is possible with the current implementation but currently it can occur only when someone else has changed the in-file settings between the particular reads of the group. After introducing the background refreshing it is enough that the in-file settings has changed before the group is read (no matter when). |
Wouldn't it be enough to just make the loading locked? The mechanism to make sure that the settings are up-to-date is in this method: The code inside the NeedRefresh is still locked. So even when the GetValue doesn't use locks, when a refresh is required, all GetValues are locked because the settings are loaded. The GetValue method without locks would be: |
That would be enough, but this way we are still exposed to locks (and related to them slowdowns and deadlocks) while calling the |
What if (a big one):
No locks needed. |
Depending on how you are going to load the new instance (in background or in the caller thread) you will fall into one of the issues I described above. |
In fact it is already the fact because all the AppSettings getters and setters are redirected to a singleton instance of SettingsContainer. It would be enough to recreate the underlying SettingsCache only. |
But only if the settings require reloading. Since the settings hardly ever needs reloading from disk, I do not see an issue. |
4c4a84d
to
4e47e24
Compare
After (1 day) testing I got an exception that 2 threads where both reading the same file. So I partly reverted the change. The set/(re)load/Save actions now use a lock. The GetValue is not locked. ConcurrentDictionary is still used. Performance is not affected. Do you consider the change safe after this change? |
I consider this a good direction. Changes proposed here are not safe and require some more work. |
Ok, didn't know about GitExtSettingsCache. Indeed, GetValueImpl is not safe because it uses a non safe dictionary. What if I change it again, put all the locks back. But instead of normal locks, I use readerwriterslimlock? This way at least everything is save. The overhead of the readerwriterslimlock is not that big. Locking at least is minimized then. |
If you leave the |
4e47e24
to
530473b
Compare
I attempted to implement the readerwriterslimlock. Since the level of recursion required, I am not sure this is a good solution. Then I looked at the code again and did some more debugging. I now understand your remark about GetValue and TryGetValue. At first, I didn't saw what the difference is. But now I do! TryGetValue will look in the cache first, and then calls GetValue if it is not found in the cache. The GetValue doesn't try the cache first. Didn't got that from the functionnames. So, I did what you suggested earlier. I put all the locks back, except for the one in TryGetValue. The dictionary is still concurrent. Also, I added some comments to the method TryGetValue. Since the tryget is always used in the AppSettings class, the performance gain is still the same as the initial PR. Only more safe. Using the readerwriterslimlock is not needed, since locks are now hardly used. |
530473b
to
ba34e8b
Compare
Thanks for your help! Rebased and pushed to master. |
Fixes random lockups when rendering the revision graph. Profiling showed a lot of (waiting) time on the locks in the settingscache. During cell rendering, the setting 'ShowRevisionGridGraphColumn' is called extreme often. In some occasions this caused the application to freeze and not recover (waited few minutes). This also improves the scrolling speed a bit, when revision graph is visible.
Changes proposed in this pull request:
What did I do to test the code and ensure quality:
Has been tested on (remove any that don't apply):