- fix: memory leak from canalisation part 2

This commit is contained in:
2025-12-12 22:23:45 +03:00
parent f7cec5d093
commit e0a16f7fb6
10 changed files with 172 additions and 122 deletions

View File

@@ -32,6 +32,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
<PackageReference Include="libsodium" Version="1.0.20"/>
<PackageReference Include="Robust.Natives" Version="0.2.3" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
</ItemGroup>
<Target Name="BuildCheck" AfterTargets="AfterBuild">
@@ -63,8 +64,4 @@
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Controls\ServerListView.axaml" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Nebula.Launcher.Controls;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;

View File

@@ -118,6 +118,14 @@ public sealed class ServerViewContainer
private readonly List<string> _favorites = [];
private readonly Dictionary<string, string> _customNames = [];
private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new();
public ICollection<IListEntryModelView> Items =>
_entries.Values
.Select(wr => wr.TryGetTarget(out var target) ? target : null)
.Where(t => t != null)
.ToList()!;
public ServerViewContainer()
{
_viewHelperService = new ViewHelperService();
@@ -131,108 +139,124 @@ public sealed class ServerViewContainer
configurationService.SubscribeVarChanged(LauncherConVar.ServerCustomNames, OnCustomNamesChanged, true);
}
private void OnCustomNamesChanged(Dictionary<string,string>? value)
{
var oldNames =
_customNames.ToDictionary(k => k.Key, v => v.Value); //Clone think
_customNames.Clear();
if(value == null)
{
foreach (var (ip,_) in oldNames)
{
if(!_entries.TryGetValue(ip, out var listEntry) || listEntry is not IEntryNameHolder entryNameHolder)
continue;
entryNameHolder.Name = null;
}
return;
}
foreach (var (oldIp, oldName) in oldNames)
{
if(value.TryGetValue(oldIp, out var newName))
{
if (oldName == newName)
value.Remove(newName);
continue;
}
if(!_entries.TryGetValue(oldIp, out var listEntry) ||
listEntry is not IEntryNameHolder entryNameHolder)
continue;
entryNameHolder.Name = null;
}
foreach (var (ip, name) in value)
{
_customNames.Add(ip, name);
if(!_entries.TryGetValue(ip, out var listEntry) || listEntry is not IEntryNameHolder entryNameHolder)
continue;
entryNameHolder.Name = name;
}
}
private void OnFavoritesChange(string[]? value)
{
_favorites.Clear();
if(value == null) return;
foreach (var favorite in value)
{
_favorites.Add(favorite);
if (_entries.TryGetValue(favorite, out var entry) && entry is IFavoriteEntryModelView favoriteView)
{
favoriteView.IsFavorite = true;
}
}
}
private readonly Dictionary<string, IListEntryModelView> _entries = new();
public ICollection<IListEntryModelView> Items => _entries.Values;
public void Clear()
{
foreach (var (_, value) in _entries)
foreach (var (_, weakRef) in _entries)
{
value.Dispose();
if (weakRef.TryGetTarget(out var value))
value.Dispose();
}
_entries.Clear();
}
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
{
var key = url.ToString();
IListEntryModelView? entry;
lock (_entries)
{
_customNames.TryGetValue(url.ToString(), out var customName);
if (_entries.TryGetValue(url.ToString(), out entry))
_customNames.TryGetValue(key, out var customName);
if (_entries.TryGetValue(key, out var weakEntry)
&& weakEntry.TryGetTarget(out entry))
{
return entry;
}
if (serverStatus is not null)
entry = _viewHelperService.GetViewModel<ServerEntryModelView>().WithData(url, customName, serverStatus);
{
entry = _viewHelperService
.GetViewModel<ServerEntryModelView>()
.WithData(url, customName, serverStatus);
}
else
entry = _viewHelperService.GetViewModel<ServerCompoundEntryViewModel>().LoadServerEntry(url, customName, CancellationToken.None);
if(_favorites.Contains(url.ToString()) &&
entry is IFavoriteEntryModelView favoriteEntryModelView)
favoriteEntryModelView.IsFavorite = true;
_entries.Add(url.ToString(), entry);
{
entry = _viewHelperService
.GetViewModel<ServerCompoundEntryViewModel>()
.LoadServerEntry(url, customName, CancellationToken.None);
}
if (_favorites.Contains(key)
&& entry is IFavoriteEntryModelView fav)
{
fav.IsFavorite = true;
}
_entries[key] = new WeakReference<IListEntryModelView>(entry);
}
return entry;
}
private void OnFavoritesChange(string[]? value)
{
_favorites.Clear();
if (value == null) return;
foreach (var favorite in value)
{
_favorites.Add(favorite);
if (_entries.TryGetValue(favorite, out var weak)
&& weak.TryGetTarget(out var entry)
&& entry is IFavoriteEntryModelView fav)
{
fav.IsFavorite = true;
}
}
}
private void OnCustomNamesChanged(Dictionary<string, string>? value)
{
var oldNames = _customNames.ToDictionary(x => x.Key, x => x.Value);
_customNames.Clear();
if (value == null)
{
foreach (var (ip, _) in oldNames)
{
ResetName(ip);
}
return;
}
foreach (var (oldIp, oldName) in oldNames)
{
if (value.TryGetValue(oldIp, out var newName))
{
if (oldName == newName)
value.Remove(newName);
continue;
}
ResetName(oldIp);
}
foreach (var (ip, name) in value)
{
_customNames.Add(ip, name);
if (_entries.TryGetValue(ip, out var weak)
&& weak.TryGetTarget(out var entry)
&& entry is IEntryNameHolder holder)
{
holder.Name = name;
}
}
}
private void ResetName(string ip)
{
if (_entries.TryGetValue(ip, out var weak)
&& weak.TryGetTarget(out var entry)
&& entry is IEntryNameHolder holder)
{
holder.Name = null;
}
}
}
public interface IListEntryModelView : IDisposable

View File

@@ -1,7 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Nebula.Launcher.Models;
@@ -19,7 +18,7 @@ namespace Nebula.Launcher.ViewModels;
public sealed partial class ServerCompoundEntryViewModel :
ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder
{
[ObservableProperty] private ServerEntryModelView? _currentEntry;
private ServerEntryModelView? _currentEntry;
[ObservableProperty] private string _message = "Loading server entry...";
[ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _loading = true;
@@ -28,6 +27,28 @@ public sealed partial class ServerCompoundEntryViewModel :
private RobustUrl? _url;
private ServerFilter? _currentFilter;
public ServerEntryModelView? CurrentEntry
{
get => _currentEntry;
set
{
if (value == _currentEntry) return;
_currentEntry = value;
if (_currentEntry != null)
{
_currentEntry.IsFavorite = IsFavorite;
_currentEntry.Name = Name;
_currentEntry.ProcessFilter(_currentFilter);
}
Loading = _currentEntry == null;
OnPropertyChanged();
}
}
public string? Name
{
get => _name;
@@ -54,31 +75,43 @@ public sealed partial class ServerCompoundEntryViewModel :
{
}
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url,string? name, CancellationToken cancellationToken)
public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryModelView? entry)
{
Task.Run(async () =>
{
_url = url;
try
{
Message = "Loading server entry...";
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, cancellationToken);
CurrentEntry = ServiceProvider.GetService<ServerEntryModelView>()!.WithData(_url, name, status);
CurrentEntry.IsFavorite = IsFavorite;
CurrentEntry.Loading = false;
CurrentEntry.ProcessFilter(_currentFilter);
Loading = false;
}
catch (Exception e)
{
Message = e.Message;
}
}, cancellationToken);
CurrentEntry = entry;
return this;
}
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url, string? name, CancellationToken cancellationToken)
{
_url = url;
_name = name;
Task.Run(LoadServer, cancellationToken);
return this;
}
private async Task LoadServer()
{
if (_url is null)
{
Message = "Url is not set";
return;
}
try
{
Message = "Loading server entry...";
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, CancellationToken.None);
CurrentEntry = ServiceProvider.GetService<ServerEntryModelView>()!.WithData(_url, null, status);
Loading = false;
}
catch (Exception e)
{
Message = "Error while fetching data from " + _url + " : " + e.Message;
}
}
public void ToggleFavorites()
{
if (CurrentEntry is null && _url is not null)

View File

@@ -28,8 +28,6 @@ public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsume
[ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _isVisible;
[ObservableProperty] private bool _runVisible = true;
[ObservableProperty] private bool _tagDataVisible;
[ObservableProperty] private bool _loading;
[ObservableProperty] private string _realName;
public string? Name
@@ -131,7 +129,7 @@ public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsume
OnPropertyChanged(nameof(Status));
}
public ServerEntryModelView WithData(RobustUrl url, string? name,ServerStatus serverStatus)
public ServerEntryModelView WithData(RobustUrl url, string? name, ServerStatus serverStatus)
{
Address = url;
SetStatus(serverStatus);

View File

@@ -70,9 +70,9 @@
</Grid>
</Border>
<Panel IsVisible="{Binding !Loading}">
<views:ServerEntryView IsVisible="{Binding !Loading}" DataContext="{Binding CurrentEntry}"/>
</Panel>
<ContentControl
IsVisible="{Binding !Loading}"
Content="{Binding CurrentEntry}"/>
</Panel>
</UserControl>

View File

@@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:Nebula.Launcher.Controls"
xmlns:services="clr-namespace:Nebula.Launcher.Services"
xmlns:viewModels1="clr-namespace:Nebula.Launcher.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"