Skip to content

Commit fe741d7

Browse files
authored
Merge pull request LykosAI#750 from ionite34/disposables-improvement
Add IDisposable return for RelayPropertyFor subscriptions, and use dispose for transients
2 parents 8184aad + 182c415 commit fe741d7

11 files changed

+217
-41
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
2222
### Fixed
2323
- Fixed "The version of the native libSkiaSharp library (88.1) is incompatible with this version of SkiaSharp." error for Linux users
2424
- Fixed download links for IPAdapters in the HuggingFace model browser
25+
- Fixed potential memory leak of transient controls (Inference Prompt and Output Image Viewer) not being garbage collected due to event subscriptions
2526

2627
## v2.12.0-dev.2
2728
### Added
@@ -58,6 +59,7 @@ and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2
5859
## v2.11.6
5960
### Fixed
6061
- Fixed incorrect IPAdapter download links in the HuggingFace model browser
62+
- Fixed potential memory leak of transient controls (Inference Prompt and Output Image Viewer) not being garbage collected due to event subscriptions
6163

6264
## v2.11.5
6365
### Added
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Reactive.Disposables;
3+
using JetBrains.Annotations;
4+
5+
namespace StabilityMatrix.Avalonia.ViewModels.Base;
6+
7+
public abstract class DisposableLoadableViewModelBase : LoadableViewModelBase, IDisposable
8+
{
9+
private readonly CompositeDisposable instanceDisposables = new();
10+
11+
/// <summary>
12+
/// Adds a disposable to be disposed when this view model is disposed.
13+
/// </summary>
14+
/// <param name="disposable">The disposable to add.</param>
15+
protected void AddDisposable([HandlesResourceDisposal] IDisposable disposable)
16+
{
17+
instanceDisposables.Add(disposable);
18+
}
19+
20+
/// <summary>
21+
/// Adds disposables to be disposed when this view model is disposed.
22+
/// </summary>
23+
/// <param name="disposables">The disposables to add.</param>
24+
protected void AddDisposable([HandlesResourceDisposal] params IDisposable[] disposables)
25+
{
26+
foreach (var disposable in disposables)
27+
{
28+
instanceDisposables.Add(disposable);
29+
}
30+
}
31+
32+
/// <summary>
33+
/// Adds a disposable to be disposed when this view model is disposed.
34+
/// </summary>
35+
/// <param name="disposable">The disposable to add.</param>
36+
/// <typeparam name="T">The type of the disposable.</typeparam>
37+
/// <returns>The disposable that was added.</returns>
38+
protected T AddDisposable<T>([HandlesResourceDisposal] T disposable)
39+
where T : IDisposable
40+
{
41+
instanceDisposables.Add(disposable);
42+
return disposable;
43+
}
44+
45+
/// <summary>
46+
/// Adds disposables to be disposed when this view model is disposed.
47+
/// </summary>
48+
/// <param name="disposables">The disposables to add.</param>
49+
/// <typeparam name="T">The type of the disposables.</typeparam>
50+
/// <returns>The disposables that were added.</returns>
51+
protected T[] AddDisposable<T>([HandlesResourceDisposal] params T[] disposables)
52+
where T : IDisposable
53+
{
54+
foreach (var disposable in disposables)
55+
{
56+
instanceDisposables.Add(disposable);
57+
}
58+
59+
return disposables;
60+
}
61+
62+
protected virtual void Dispose(bool disposing)
63+
{
64+
if (disposing)
65+
{
66+
instanceDisposables.Dispose();
67+
}
68+
}
69+
70+
public void Dispose()
71+
{
72+
Dispose(true);
73+
GC.SuppressFinalize(this);
74+
}
75+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Reactive.Disposables;
3+
using JetBrains.Annotations;
4+
5+
namespace StabilityMatrix.Avalonia.ViewModels.Base;
6+
7+
public abstract class DisposableViewModelBase : ViewModelBase, IDisposable
8+
{
9+
private readonly CompositeDisposable instanceDisposables = new();
10+
11+
/// <summary>
12+
/// Adds a disposable to be disposed when this view model is disposed.
13+
/// </summary>
14+
/// <param name="disposable">The disposable to add.</param>
15+
protected void AddDisposable([HandlesResourceDisposal] IDisposable disposable)
16+
{
17+
instanceDisposables.Add(disposable);
18+
}
19+
20+
/// <summary>
21+
/// Adds disposables to be disposed when this view model is disposed.
22+
/// </summary>
23+
/// <param name="disposables">The disposables to add.</param>
24+
protected void AddDisposable([HandlesResourceDisposal] params IDisposable[] disposables)
25+
{
26+
foreach (var disposable in disposables)
27+
{
28+
instanceDisposables.Add(disposable);
29+
}
30+
}
31+
32+
/// <summary>
33+
/// Adds a disposable to be disposed when this view model is disposed.
34+
/// </summary>
35+
/// <param name="disposable">The disposable to add.</param>
36+
/// <typeparam name="T">The type of the disposable.</typeparam>
37+
/// <returns>The disposable that was added.</returns>
38+
protected T AddDisposable<T>([HandlesResourceDisposal] T disposable)
39+
where T : IDisposable
40+
{
41+
instanceDisposables.Add(disposable);
42+
return disposable;
43+
}
44+
45+
/// <summary>
46+
/// Adds disposables to be disposed when this view model is disposed.
47+
/// </summary>
48+
/// <param name="disposables">The disposables to add.</param>
49+
/// <typeparam name="T">The type of the disposables.</typeparam>
50+
/// <returns>The disposables that were added.</returns>
51+
protected T[] AddDisposable<T>([HandlesResourceDisposal] params T[] disposables)
52+
where T : IDisposable
53+
{
54+
foreach (var disposable in disposables)
55+
{
56+
instanceDisposables.Add(disposable);
57+
}
58+
59+
return disposables;
60+
}
61+
62+
protected virtual void Dispose(bool disposing)
63+
{
64+
if (disposing)
65+
{
66+
instanceDisposables.Dispose();
67+
}
68+
}
69+
70+
public void Dispose()
71+
{
72+
Dispose(true);
73+
GC.SuppressFinalize(this);
74+
}
75+
}

StabilityMatrix.Avalonia/ViewModels/Base/InferenceGenerationViewModelBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ RunningPackageService runningPackageService
9393
ClientManager = inferenceClientManager;
9494

9595
ImageGalleryCardViewModel = vmFactory.Get<ImageGalleryCardViewModel>();
96-
ImageFolderCardViewModel = vmFactory.Get<ImageFolderCardViewModel>();
96+
ImageFolderCardViewModel = AddDisposable(vmFactory.Get<ImageFolderCardViewModel>());
9797

9898
GenerateImageCommand.WithConditionalNotificationErrorHandler(notificationService);
9999
}

StabilityMatrix.Avalonia/ViewModels/Base/InferenceTabViewModelBase.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@
3333
namespace StabilityMatrix.Avalonia.ViewModels.Base;
3434

3535
public abstract partial class InferenceTabViewModelBase
36-
: LoadableViewModelBase,
37-
IDisposable,
36+
: DisposableLoadableViewModelBase,
3837
IPersistentViewProvider,
3938
IDropTarget
4039
{
@@ -158,21 +157,16 @@ private async Task DebugLoadViewState()
158157
}
159158
}
160159

161-
protected virtual void Dispose(bool disposing)
160+
protected override void Dispose(bool disposing)
162161
{
162+
base.Dispose(disposing);
163+
163164
if (disposing)
164165
{
165166
((IPersistentViewProvider)this).AttachedPersistentView = null;
166167
}
167168
}
168169

169-
/// <inheritdoc />
170-
public void Dispose()
171-
{
172-
Dispose(true);
173-
GC.SuppressFinalize(this);
174-
}
175-
176170
/// <summary>
177171
/// Loads image and metadata from a file path
178172
/// </summary>

StabilityMatrix.Avalonia/ViewModels/Inference/ImageFolderCardViewModel.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
3636
[View(typeof(ImageFolderCard))]
3737
[ManagedService]
3838
[Transient]
39-
public partial class ImageFolderCardViewModel : ViewModelBase
39+
public partial class ImageFolderCardViewModel : DisposableViewModelBase
4040
{
4141
private readonly ILogger<ImageFolderCardViewModel> logger;
4242
private readonly IImageIndexService imageIndexService;
@@ -86,11 +86,13 @@ ServiceManager<ViewModelBase> vmFactory
8686
.Bind(LocalImages)
8787
.Subscribe();
8888

89-
settingsManager.RelayPropertyFor(
90-
this,
91-
vm => vm.ImageSize,
92-
settings => settings.InferenceImageSize,
93-
delay: TimeSpan.FromMilliseconds(250)
89+
AddDisposable(
90+
settingsManager.RelayPropertyFor(
91+
this,
92+
vm => vm.ImageSize,
93+
settings => settings.InferenceImageSize,
94+
delay: TimeSpan.FromMilliseconds(250)
95+
)
9496
);
9597
}
9698

StabilityMatrix.Avalonia/ViewModels/Inference/InferenceTextToImageViewModel.cs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ RunningPackageService runningPackageService
8383
samplerCard.DenoiseStrength = 1.0d;
8484
});
8585

86-
PromptCardViewModel = vmFactory.Get<PromptCardViewModel>();
86+
PromptCardViewModel = AddDisposable(vmFactory.Get<PromptCardViewModel>());
87+
8788
BatchSizeCardViewModel = vmFactory.Get<BatchSizeCardViewModel>();
8889

8990
ModulesCardViewModel = vmFactory.Get<StackEditableCardViewModel>(modulesCard =>
@@ -109,13 +110,15 @@ RunningPackageService runningPackageService
109110
);
110111

111112
// When refiner is provided in model card, enable for sampler
112-
ModelCardViewModel
113-
.WhenPropertyChanged(x => x.IsRefinerSelectionEnabled)
114-
.Subscribe(e =>
115-
{
116-
SamplerCardViewModel.IsRefinerStepsEnabled =
117-
e.Sender is { IsRefinerSelectionEnabled: true, SelectedRefiner: not null };
118-
});
113+
AddDisposable(
114+
ModelCardViewModel
115+
.WhenPropertyChanged(x => x.IsRefinerSelectionEnabled)
116+
.Subscribe(e =>
117+
{
118+
SamplerCardViewModel.IsRefinerStepsEnabled =
119+
e.Sender is { IsRefinerSelectionEnabled: true, SelectedRefiner: not null };
120+
})
121+
);
119122
}
120123

121124
/// <inheritdoc />

StabilityMatrix.Avalonia/ViewModels/Inference/PromptCardViewModel.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ namespace StabilityMatrix.Avalonia.ViewModels.Inference;
2828
[View(typeof(PromptCard))]
2929
[ManagedService]
3030
[Transient]
31-
public partial class PromptCardViewModel : LoadableViewModelBase, IParametersLoadableState, IComfyStep
31+
public partial class PromptCardViewModel
32+
: DisposableLoadableViewModelBase,
33+
IParametersLoadableState,
34+
IComfyStep
3235
{
3336
private readonly IModelIndexService modelIndexService;
3437
private readonly ISettingsManager settingsManager;
@@ -75,11 +78,13 @@ SharedState sharedState
7578
vm.AvailableModules = [typeof(PromptExpansionModule)];
7679
});
7780

78-
settingsManager.RelayPropertyFor(
79-
this,
80-
vm => vm.IsAutoCompletionEnabled,
81-
settings => settings.IsPromptCompletionEnabled,
82-
true
81+
AddDisposable(
82+
settingsManager.RelayPropertyFor(
83+
this,
84+
vm => vm.IsAutoCompletionEnabled,
85+
settings => settings.IsPromptCompletionEnabled,
86+
true
87+
)
8388
);
8489
}
8590

StabilityMatrix.Core/Services/ISettingsManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public interface ISettingsManager
7373
/// <summary>
7474
/// Register a source observable object and property to be relayed to Settings
7575
/// </summary>
76-
void RelayPropertyFor<T, TValue>(
76+
IDisposable RelayPropertyFor<T, TValue>(
7777
T source,
7878
Expression<Func<T, TValue>> sourceProperty,
7979
Expression<Func<Settings, TValue>> settingsProperty,

StabilityMatrix.Core/Services/SettingsManager.cs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.ComponentModel;
22
using System.Diagnostics.CodeAnalysis;
33
using System.Linq.Expressions;
4+
using System.Reactive.Disposables;
45
using System.Reactive.Linq;
56
using System.Reflection;
67
using System.Text.Json;
@@ -163,7 +164,7 @@ public void Transaction<TValue>(Expression<Func<Settings, TValue>> expression, T
163164
}
164165

165166
/// <inheritdoc />
166-
public void RelayPropertyFor<T, TValue>(
167+
public IDisposable RelayPropertyFor<T, TValue>(
167168
T source,
168169
Expression<Func<T, TValue>> sourceProperty,
169170
Expression<Func<Settings, TValue>> settingsProperty,
@@ -181,7 +182,7 @@ public void RelayPropertyFor<T, TValue>(
181182
var sourceTypeName = source.GetType().Name;
182183

183184
// Update source when settings change
184-
SettingsPropertyChanged += (sender, args) =>
185+
void OnSettingsPropertyChanged(object? sender, RelayPropertyChangedEventArgs args)
185186
{
186187
if (args.PropertyName != settingsPropertyPath)
187188
return;
@@ -198,10 +199,10 @@ public void RelayPropertyFor<T, TValue>(
198199
);
199200

200201
sourceInstanceAccessor.Set(source, settingsAccessor.Get(Settings));
201-
};
202+
}
202203

203204
// Set and Save settings when source changes
204-
source.PropertyChanged += (sender, args) =>
205+
void OnSourcePropertyChanged(object? sender, PropertyChangedEventArgs args)
205206
{
206207
if (args.PropertyName != sourcePropertyPath)
207208
return;
@@ -241,13 +242,32 @@ public void RelayPropertyFor<T, TValue>(
241242
sender,
242243
new RelayPropertyChangedEventArgs(settingsPropertyPath, true)
243244
);
244-
};
245+
}
246+
247+
var subscription = Disposable.Create(() =>
248+
{
249+
source.PropertyChanged -= OnSourcePropertyChanged;
250+
SettingsPropertyChanged -= OnSettingsPropertyChanged;
251+
});
245252

246-
// Set initial value if requested
247-
if (setInitial)
253+
try
248254
{
249-
sourceInstanceAccessor.Set(settingsAccessor.Get(Settings));
255+
SettingsPropertyChanged += OnSettingsPropertyChanged;
256+
source.PropertyChanged += OnSourcePropertyChanged;
257+
258+
// Set initial value if requested
259+
if (setInitial)
260+
{
261+
sourceInstanceAccessor.Set(settingsAccessor.Get(Settings));
262+
}
250263
}
264+
catch
265+
{
266+
subscription.Dispose();
267+
throw;
268+
}
269+
270+
return subscription;
251271
}
252272

253273
/// <inheritdoc />

0 commit comments

Comments
 (0)