Skip to content

Conversation

@abasau-incomm
Copy link

@abasau-incomm abasau-incomm commented Aug 21, 2025

While updating Handlebars.Net from 1.x to 2.1.6, I have noticed that GC Handler count increases significantly with every compiled template. GC Handlers are associated with WeakReference objects which were introduced in #456.

I discovered a couple of issues while investigating the GC Handlers increase.

My app complies templates using the static method Handlebars.Compile().

Before the Update

image

After the Update

image

Comment on lines 185 to +189
if (_comparer.Equals(key, entries[i].Key))
{
entries[i].Value = value;
return;
}
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

Comment on lines 47 to 51
public bool Return(BindingContext item)
{
item.Configuration = null;

item.Root = null;
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

@rexm
Copy link
Member

rexm commented Aug 22, 2025

Thanks for working on this, I’m traveling but will keep an eye on it!

Comment on lines 14 to 18
public void Add(T value)
{
// Need a way to reset _firstAvailableIndex periodically

for (var index = _firstAvailableIndex; index < _store.Count; index++)
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?

@abasau-incomm abasau-incomm changed the title WeakReference garbage collection - DRAFT WeakReference garbage collection Aug 22, 2025
@abasau-incomm
Copy link
Author

Thanks for working on this, I’m traveling but will keep an eye on it!

@rexm thanks! I am still adding details to the PR and will hit "Ready for review" once done.

@abasau-incomm abasau-incomm marked this pull request as ready for review August 22, 2025 01:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants