Skip to content

Commit ad5c616

Browse files
authored
Merge pull request #456 from zjklee/issues/452
Fix memory leak at Configuration subscribers
2 parents 9e9d60e + 99ae7f0 commit ad5c616

File tree

8 files changed

+221
-52
lines changed

8 files changed

+221
-52
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Linq;
3+
using HandlebarsDotNet.Collections;
4+
using Xunit;
5+
6+
namespace HandlebarsDotNet.Test.Collections
7+
{
8+
public class WeakCollectionTests
9+
{
10+
private readonly WeakCollection<object> _collection = new WeakCollection<object>();
11+
12+
[Fact]
13+
public void ObjectIsRemovedFromCollectionOnceNotReachable()
14+
{
15+
var _0 = new object();
16+
_collection.Add(_0);
17+
18+
CallInItsOwnScope(() =>
19+
{
20+
var _1 = new object();
21+
_collection.Add(_1);
22+
23+
GC.Collect();
24+
Assert.Equal(2, _collection.Size);
25+
Assert.Equal(2, _collection.Count());
26+
});
27+
28+
GC.Collect();
29+
Assert.Equal(2, _collection.Size);
30+
Assert.Single(_collection);
31+
}
32+
33+
[Fact]
34+
public void BucketInCollectionReused()
35+
{
36+
var _0 = new object();
37+
_collection.Add(_0);
38+
39+
CallInItsOwnScope(() =>
40+
{
41+
_collection.Add(new object());
42+
_collection.Add(new object());
43+
_collection.Add(new object());
44+
});
45+
46+
GC.Collect();
47+
48+
_ = _collection.Count(); // force enumeration to set inner index
49+
50+
var _2 = new object();
51+
var _3 = new object();
52+
_collection.Add(_2);
53+
_collection.Add(_3);
54+
55+
GC.Collect();
56+
Assert.Equal(4, _collection.Size);
57+
Assert.Equal(3, _collection.Count());
58+
}
59+
60+
[Fact]
61+
public void BucketInCollectionReusedAfterRemove()
62+
{
63+
var _0 = new object();
64+
var _1 = new object();
65+
var _2 = new object();
66+
_collection.Add(_0);
67+
_collection.Add(_1);
68+
_collection.Add(_2);
69+
70+
GC.Collect();
71+
72+
_collection.Remove(_0);
73+
_collection.Remove(_1);
74+
_collection.Add(new object());
75+
76+
Assert.Equal(3, _collection.Size);
77+
Assert.Equal(2, _collection.Count());
78+
}
79+
80+
private static void CallInItsOwnScope(Action scope) => scope();
81+
}
82+
}

source/Handlebars/Collections/ObservableIndex.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ public class ObservableIndex<TKey, TValue, TComparer> :
1818
private readonly ReaderWriterLockSlim _observersLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
1919
private readonly ReaderWriterLockSlim _itemsLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
2020

21-
private readonly List<IObserver<ObservableEvent<TValue>>> _observers;
21+
private readonly WeakCollection<IObserver<ObservableEvent<TValue>>> _observers;
2222
private readonly DictionarySlim<TKey, TValue, TComparer> _inner;
2323

2424
public ObservableIndex(TComparer comparer, IReadOnlyIndexed<TKey, TValue> outer = null)
2525
{
2626
_inner = outer != null ? new DictionarySlim<TKey, TValue, TComparer>(outer, comparer) : new DictionarySlim<TKey, TValue, TComparer>(comparer);
27-
_observers = new List<IObserver<ObservableEvent<TValue>>>();
27+
_observers = new WeakCollection<IObserver<ObservableEvent<TValue>>>();
2828
if (outer is IObservable<ObservableEvent<TValue>> observableDictionary)
2929
{
3030
observableDictionary.Subscribe(this);
@@ -38,7 +38,7 @@ public IDisposable Subscribe(IObserver<ObservableEvent<TValue>> observer)
3838
_observers.Add(observer);
3939
}
4040

41-
var disposableContainer = new DisposableContainer<List<IObserver<ObservableEvent<TValue>>>, ReaderWriterLockSlim>(
41+
var disposableContainer = new DisposableContainer<WeakCollection<IObserver<ObservableEvent<TValue>>>, ReaderWriterLockSlim>(
4242
_observers, _observersLock, (observers, @lock) =>
4343
{
4444
using (@lock.WriteLock())
@@ -55,11 +55,11 @@ private void Publish(ObservableEvent<TValue> @event)
5555
{
5656
using (_observersLock.ReadLock())
5757
{
58-
for (var index = 0; index < _observers.Count; index++)
58+
foreach (var observer in _observers)
5959
{
6060
try
6161
{
62-
_observers[index].OnNext(@event);
62+
observer.OnNext(@event);
6363
}
6464
catch
6565
{

source/Handlebars/Collections/ObservableList.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class ObservableList<T> :
1616
private readonly ReaderWriterLockSlim _observersLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
1717
private readonly ReaderWriterLockSlim _itemsLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
1818

19-
private readonly List<IObserver<ObservableEvent<T>>> _observers = new List<IObserver<ObservableEvent<T>>>();
19+
private readonly WeakCollection<IObserver<ObservableEvent<T>>> _observers = new WeakCollection<IObserver<ObservableEvent<T>>>();
2020
private readonly List<T> _inner;
2121

2222
public ObservableList(IEnumerable<T> list = null)
@@ -83,7 +83,7 @@ public IDisposable Subscribe(IObserver<ObservableEvent<T>> observer)
8383
_observers.Add(observer);
8484
}
8585

86-
var disposableContainer = new DisposableContainer<List<IObserver<ObservableEvent<T>>>, ReaderWriterLockSlim>(
86+
var disposableContainer = new DisposableContainer<WeakCollection<IObserver<ObservableEvent<T>>>, ReaderWriterLockSlim>(
8787
_observers, _observersLock, (observers, @lock) =>
8888
{
8989
using (@lock.WriteLock())
@@ -123,11 +123,11 @@ private void Publish(ObservableEvent<T> @event)
123123
{
124124
using (_observersLock.ReadLock())
125125
{
126-
for (int index = 0; index < _observers.Count; index++)
126+
foreach (var observer in _observers)
127127
{
128128
try
129129
{
130-
_observers[index].OnNext(@event);
130+
observer.OnNext(@event);
131131
}
132132
catch
133133
{

source/Handlebars/Collections/ObserverBuilder.cs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,32 @@
33

44
namespace HandlebarsDotNet.Collections
55
{
6-
internal class ObserverBuilder<T>
6+
internal static class ObserverBuilder<T>
77
{
8-
private readonly Dictionary<Type, List<Action<T>>> _handlers = new Dictionary<Type, List<Action<T>>>();
9-
10-
public ObserverBuilder<T> OnEvent<TEvent>(Action<TEvent> handler, Func<TEvent, bool> predicate = null) where TEvent: T
8+
public static ObserverBuilder<T, TState> Create<TState>(TState state)
119
{
12-
if (!_handlers.TryGetValue(typeof(TEvent), out var handlers))
13-
{
14-
handlers = new List<Action<T>>();
15-
_handlers.Add(typeof(TEvent), handlers);
16-
}
17-
18-
handlers.Add(@event =>
19-
{
20-
if(predicate?.Invoke((TEvent) @event) ?? true) handler((TEvent) @event);
21-
});
22-
23-
return this;
10+
return new ObserverBuilder<T, TState>(state);
2411
}
25-
26-
public ObserverBuilder<T> OnEvent<TEvent, TState>(TState state, Action<TEvent, TState> handler, Func<TEvent, bool> predicate = null) where TEvent: T
12+
}
13+
14+
internal class ObserverBuilder<T, TState>
15+
{
16+
private readonly TState _state;
17+
private readonly Dictionary<Type, List<Action<T>>> _handlers = new Dictionary<Type, List<Action<T>>>();
18+
19+
public ObserverBuilder(TState state) => _state = state;
20+
21+
public ObserverBuilder<T, TState> OnEvent<TEvent>(Action<TEvent, TState> handler, Func<TEvent, bool> predicate = null) where TEvent: T
2722
{
2823
if (!_handlers.TryGetValue(typeof(TEvent), out var handlers))
2924
{
3025
handlers = new List<Action<T>>();
3126
_handlers.Add(typeof(TEvent), handlers);
3227
}
3328

34-
handlers.Add(@event =>
29+
handlers.Add((@event) =>
3530
{
36-
if(predicate?.Invoke((TEvent) @event) ?? true) handler((TEvent) @event, state);
31+
if(predicate?.Invoke((TEvent) @event) ?? true) handler((TEvent) @event, _state);
3732
});
3833

3934
return this;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
5+
namespace HandlebarsDotNet.Collections
6+
{
7+
public class WeakCollection<T> : IEnumerable<T> where T : class
8+
{
9+
private readonly List<WeakReference<T>> _store = new List<WeakReference<T>>();
10+
private int _firstAvailableIndex = 0;
11+
12+
public int Size => _store.Count;
13+
14+
public void Add(T value)
15+
{
16+
for (var index = _firstAvailableIndex; index < _store.Count; index++)
17+
{
18+
if (_store[index] == null)
19+
{
20+
_firstAvailableIndex = index + 1;
21+
_store[index] = new WeakReference<T>(value);
22+
return;
23+
}
24+
25+
if (!_store[index].TryGetTarget(out _))
26+
{
27+
_firstAvailableIndex = index + 1;
28+
_store[index].SetTarget(value);
29+
return;
30+
}
31+
}
32+
33+
_store.Add(new WeakReference<T>(value));
34+
_firstAvailableIndex = _store.Count;
35+
}
36+
37+
public void Remove(T value)
38+
{
39+
for (var index = 0; index < _store.Count; index++)
40+
{
41+
if (_store[index] == null)
42+
{
43+
_firstAvailableIndex = Math.Min(_firstAvailableIndex, index);
44+
continue;
45+
}
46+
47+
if (!_store[index].TryGetTarget(out var target))
48+
{
49+
_firstAvailableIndex = Math.Min(_firstAvailableIndex, index);
50+
continue;
51+
}
52+
53+
if (target.Equals(value))
54+
{
55+
_store[index] = null;
56+
_firstAvailableIndex = Math.Min(_firstAvailableIndex, index);
57+
return;
58+
}
59+
}
60+
}
61+
62+
public IEnumerator<T> GetEnumerator()
63+
{
64+
for (var index = 0; index < _store.Count; index++)
65+
{
66+
var reference = _store[index];
67+
if (reference == null)
68+
{
69+
_firstAvailableIndex = Math.Min(_firstAvailableIndex, index);
70+
continue;
71+
}
72+
73+
if (!reference.TryGetTarget(out var target))
74+
{
75+
_firstAvailableIndex = Math.Min(_firstAvailableIndex, index);
76+
continue;
77+
}
78+
79+
yield return target;
80+
}
81+
}
82+
83+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
84+
}
85+
}

source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ namespace HandlebarsDotNet
1717
{
1818
internal class HandlebarsConfigurationAdapter : ICompiledHandlebarsConfiguration
1919
{
20+
private readonly List<object> _observers = new List<object>();
21+
2022
public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
2123
{
2224
UnderlingConfiguration = configuration;
@@ -85,11 +87,8 @@ private ObservableIndex<PathInfoLight, Ref<IHelperDescriptor<TOptions>>, IEquali
8587

8688
var target = new ObservableIndex<PathInfoLight, Ref<IHelperDescriptor<TOptions>>, IEqualityComparer<PathInfoLight>>(equalityComparer, existingHelpers);
8789

88-
var helpersObserver = new ObserverBuilder<ObservableEvent<IHelperDescriptor<TOptions>>>()
89-
.OnEvent<
90-
DictionaryAddedObservableEvent<string, IHelperDescriptor<TOptions>>,
91-
ObservableIndex<PathInfoLight, Ref<IHelperDescriptor<TOptions>>, IEqualityComparer<PathInfoLight>>
92-
>(target,
90+
var observer = ObserverBuilder<ObservableEvent<IHelperDescriptor<TOptions>>>.Create(target)
91+
.OnEvent<DictionaryAddedObservableEvent<string, IHelperDescriptor<TOptions>>>(
9392
(@event, state) =>
9493
{
9594
PathInfoLight key = $"[{@event.Key}]";
@@ -103,7 +102,9 @@ private ObservableIndex<PathInfoLight, Ref<IHelperDescriptor<TOptions>>, IEquali
103102
})
104103
.Build();
105104

106-
source.As<ObservableIndex<string, IHelperDescriptor<TOptions>, StringEqualityComparer>>()?.Subscribe(helpersObserver);
105+
_observers.Add(observer);
106+
107+
source.As<ObservableIndex<string, IHelperDescriptor<TOptions>, StringEqualityComparer>>()?.Subscribe(observer);
107108

108109
return target;
109110
}
@@ -125,13 +126,12 @@ private ObservableList<IObjectDescriptorProvider> CreateObjectDescriptorProvider
125126
}
126127
.AddMany(descriptorProviders);
127128

128-
var observer = new ObserverBuilder<ObservableEvent<IObjectDescriptorProvider>>()
129-
.OnEvent<
130-
AddedObservableEvent<IObjectDescriptorProvider>,
131-
ObservableList<IObjectDescriptorProvider>
132-
>(objectDescriptorProviders, (@event, state) => { state.Add(@event.Value); })
129+
var observer = ObserverBuilder<ObservableEvent<IObjectDescriptorProvider>>.Create(objectDescriptorProviders)
130+
.OnEvent<AddedObservableEvent<IObjectDescriptorProvider>>((@event, state) => { state.Add(@event.Value); })
133131
.Build();
134132

133+
_observers.Add(observer);
134+
135135
descriptorProviders.Subscribe(observer);
136136

137137
return objectDescriptorProviders;

source/Handlebars/IO/Formatters/FormatterProvider.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,20 @@ public class FormatterProvider : IFormatterProvider
3434
private readonly LookupSlim _formatters = new LookupSlim(new ReferenceEqualityComparer<Type>());
3535

3636
private readonly ObservableList<IFormatterProvider> _formatterProviders;
37+
private readonly List<object> _observers = new List<object>();
3738

3839
public FormatterProvider(ObservableList<IFormatterProvider> providers = null)
3940
{
4041
_formatterProviders = new ObservableList<IFormatterProvider>();
4142

4243
if (providers != null) Append(providers);
4344

44-
var observer = new ObserverBuilder<ObservableEvent<IFormatterProvider>>()
45-
.OnEvent<AddedObservableEvent<IFormatterProvider>, LookupSlim>(
46-
_formatters, (@event, state) => state.Clear()
47-
)
45+
var observer = ObserverBuilder<ObservableEvent<IFormatterProvider>>.Create(_formatters)
46+
.OnEvent<AddedObservableEvent<IFormatterProvider>>((@event, state) => state.Clear())
4847
.Build();
4948

49+
_observers.Add(observer);
50+
5051
_formatterProviders.Subscribe(observer);
5152
}
5253

0 commit comments

Comments
 (0)