Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions source/Handlebars/Collections/DictionarySlim.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ public void AddOrReplace(in TKey key, in TValue value)
(uint)i < (uint)entries.Length; i = entries[i].Next)
{
if (_comparer.Equals(key, entries[i].Key))
{
entries[i].Value = value;
return;
}
Comment on lines 185 to +189
Copy link
Author

@abasau-incomm abasau-incomm Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddOrReplace() method always adds a key-value pair even if the key was found and replaced in the dictionary. This issue can be observed by inspecting the configuration object of Handlebars with a shared environment (Handlebars.CreateSharedEnvironment(configuration)) after multiple compilations.

image image

if (collisionCount == entries.Length)
{
// The chain of entries forms a loop; which means a concurrent update has happened.
Expand Down
2 changes: 2 additions & 0 deletions source/Handlebars/Collections/WeakCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class WeakCollection<T> : IEnumerable<T> where T : class

public void Add(T value)
{
// Need a way to reset _firstAvailableIndex periodically

for (var index = _firstAvailableIndex; index < _store.Count; index++)
Comment on lines 14 to 18
Copy link
Author

@abasau-incomm abasau-incomm Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this spot is the main source of constant growth of GC Handlers and WeakReferences. I don't have a good idea about how to fix it and I'd appreciate some guidance.

This issue can only be observed if a static method Handlebars.Compile(source) is used, or if a Handlebars object is stored in a static variable (e.g. private static IHandlebars _handlebars = Handlebars.Create();), or HandlebarsConfiguration object is stored in a static variable.

Every time when a template is compiled, new instances of FormatterProvider and ObjectDescriptorFactory are created on:

var formatterProvider = new FormatterProvider(configuration.FormatterProviders);
var objectDescriptorFactory = new ObjectDescriptorFactory(configuration.ObjectDescriptorProviders);

Those new instances are then subscribed to the original FormatterProvider and ObjectDescriptorFactory objects from the main configuration object.

WeakCollection has a mechanism to detect dead references and re-use WeakReference objects to store new values. It's done by resetting _firstAvailableIndex in Remove() and GetEnumerator(). But, if those two methods are never called, List<WeakReference<T> grows continuously.

image image

ObservableIndex and ObservableList that utilize WeakCollection have Subscribe() methods which return a disposable container which calls Remove() when disposed.

public IDisposable Subscribe(IObserver<ObservableEvent<TValue>> observer)
{
using (_observersLock.WriteLock())
{
_observers.Add(observer);
}
var disposableContainer = new DisposableContainer<WeakCollection<IObserver<ObservableEvent<TValue>>>, ReaderWriterLockSlim>(
_observers, _observersLock, (observers, @lock) =>
{
using (@lock.WriteLock())
{
observers.Remove(this);
}
}
);
return disposableContainer;
}

But none of the methods that call Subscribe() save those disposable containers and therefore don't dispose them.

So maybe the fix should be to keep track of disposable containers in the methods that call Subscribe() and dispose them in finalizers?

{
if (_store[index] == null)
Expand Down
2 changes: 2 additions & 0 deletions source/Handlebars/Pools/BindingContext.Pool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ internal struct BindingContextPolicy : IInternalObjectPoolPolicy<BindingContext>

public bool Return(BindingContext item)
{
item.Configuration = null;

item.Root = null;
Comment on lines 47 to 51
Copy link
Author

@abasau-incomm abasau-incomm Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BindingContextPool may temporary prevent garbage collection of some configuration objects because the Configuration property isn't cleared when BindingContext is being returned to the pool. It's unlikely that this issue will cause actual problems because the pool will eventually cycle through all BindingContext objects and it will release the configuration object by assigning a new configuration object on:

context.Configuration = configuration;

But it makes sense to clear Configuration property when returning BindingContext to the pool because this property will be set to a new value in the CreateContext() method. It's better to clear it explicitly as not to prevent GC.

image

item.Value = null;
item.ParentContext = null;
Expand Down
Loading