Skip to content
Merged
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
23 changes: 20 additions & 3 deletions source/Handlebars/BindingContext.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,7 @@ internal void SetDataObject(object? data)
private void Initialize()
{
Root = ParentContext?.Root ?? this;
HasFrameHelpers = ParentContext?.HasFrameHelpers ?? false;

ContextDataObject.AddOrReplace(ChainSegment.Root, Root.Value, out WellKnownVariables[(int) WellKnownVariable.Root]);

Expand DownExpand Up@@ -117,9 +118,17 @@ out WellKnownVariables[(int) WellKnownVariable.Parent]
internal CascadeIndex<string, IHelperDescriptor<BlockHelperOptions>, StringEqualityComparer> BlockHelpers { get; }

internal TemplateDelegate? PartialBlockTemplate { get; set; }

internal short PartialDepth { get; set; }

/// <summary>
/// <c>true</c> once any code obtained this frame's (or an ancestor frame's) helper
/// registries for writing — see <see cref="IHelpersRegistry"/>. Until then the
/// per-frame helper chain is known to be empty, letting hot paths
/// (<see cref="Helpers.LateBindHelperDescriptor"/>) skip the cascade lookup entirely.
/// </summary>
internal bool HasFrameHelpers { get; set; }

public object? Value { get; set; }

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand DownExpand Up@@ -203,8 +212,16 @@ private static void PopulateHash(HashParameterDictionary hash, object from)
}
}

IIndexed<string, IHelperDescriptor<HelperOptions>> IHelpersRegistry.GetHelpers() => Helpers;
IIndexed<string, IHelperDescriptor<HelperOptions>> IHelpersRegistry.GetHelpers()
{
HasFrameHelpers = true;
return Helpers;
}

IIndexed<string, IHelperDescriptor<BlockHelperOptions>> IHelpersRegistry.GetBlockHelpers() => BlockHelpers;
IIndexed<string, IHelperDescriptor<BlockHelperOptions>> IHelpersRegistry.GetBlockHelpers()
{
HasFrameHelpers = true;
return BlockHelpers;
}
}
}
4 changes: 2 additions & 2 deletions source/Handlebars/Decorators/BlockDecoratorOptions.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,8 @@ public void Template(in EncodedTextWriter writer, object? context)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Template(in EncodedTextWriter writer, BindingContext context) => OriginalTemplate(writer, context);

IIndexed<string, IHelperDescriptor<HelperOptions>> IHelpersRegistry.GetHelpers() => Frame.Helpers;
IIndexed<string, IHelperDescriptor<HelperOptions>> IHelpersRegistry.GetHelpers() => ((IHelpersRegistry) Frame).GetHelpers();

IIndexed<string, IHelperDescriptor<BlockHelperOptions>> IHelpersRegistry.GetBlockHelpers() => Frame.BlockHelpers;
IIndexed<string, IHelperDescriptor<BlockHelperOptions>> IHelpersRegistry.GetBlockHelpers() => ((IHelpersRegistry) Frame).GetBlockHelpers();
}
}
4 changes: 2 additions & 2 deletions source/Handlebars/Decorators/DecoratorOptions.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,8 +25,8 @@ BindingContext frame

public PathInfo Name { get; }

IIndexed<string, IHelperDescriptor<HelperOptions>> IHelpersRegistry.GetHelpers() => Frame.Helpers;
IIndexed<string, IHelperDescriptor<HelperOptions>> IHelpersRegistry.GetHelpers() => ((IHelpersRegistry) Frame).GetHelpers();

IIndexed<string, IHelperDescriptor<BlockHelperOptions>> IHelpersRegistry.GetBlockHelpers() => Frame.BlockHelpers;
IIndexed<string, IHelperDescriptor<BlockHelperOptions>> IHelpersRegistry.GetBlockHelpers() => ((IHelpersRegistry) Frame).GetBlockHelpers();
}
}
9 changes: 8 additions & 1 deletion source/Handlebars/Extensions/EnumerableExtensions.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,14 @@ internal static class EnumerableExtensions
public static bool Any(this IEnumerable builder)
{
var enumerator = builder.GetEnumerator();
return enumerator.MoveNext();
try
{
return enumerator.MoveNext();
}
finally
{
(enumerator as IDisposable)?.Dispose();
}
}

public static bool IsOneOf<TSource, TExpected>(this IEnumerable<TSource> source)
Expand Down
40 changes: 22 additions & 18 deletions source/Handlebars/HandlebarsUtils.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,25 @@ public static bool IsFalsy([NotNullWhen(false)] object? value, bool includeZero)
return IsFalsyJsonElement(element, includeZero);
}

if (IsNumber(value) && !includeZero)
if (includeZero) return false;

// Typed zero checks in likelihood order; avoids IsNumber's isinst cascade
// followed by Convert.ToBoolean's IConvertible dispatch on the hot path.
return value switch
{
return !Convert.ToBoolean(value);
}
return false;
int i => i == 0,
long l => l == 0L,
double d => d == 0d,
float f => f == 0f,
decimal m => m == 0m,
byte b8 => b8 == 0,
sbyte s8 => s8 == 0,
short s16 => s16 == 0,
ushort u16 => u16 == 0,
uint u32 => u32 == 0u,
ulong u64 => u64 == 0ul,
_ => false
};
}

private static bool IsFalsyJsonElement(JsonElement element, bool includeZero)
Expand DownExpand Up@@ -84,23 +98,13 @@ public static bool IsFalsyOrEmpty([NotNullWhen(false)] object? value, bool inclu
|| (element.ValueKind == JsonValueKind.Array && element.GetArrayLength() == 0);
}

// O(1) emptiness checks that avoid allocating an enumerator for the common
// collection shapes; the IEnumerable fallback boxes List<T>-style struct enumerators.
if (value is ICollection collection) return collection.Count == 0;

return value is IEnumerable enumerable && !enumerable.Any();
}

private static bool IsNumber([NotNullWhen(true)] object? value)
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
}
}
}

Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
using System;
using System.Runtime.CompilerServices;
using HandlebarsDotNet.Collections;
using HandlebarsDotNet.PathStructure;

namespace HandlebarsDotNet.Helpers.BlockHelpers
{
public sealed class LateBindBlockHelperDescriptor : IHelperDescriptor<BlockHelperOptions>
{
// See LateBindHelperDescriptor: ObservableList.Count locks per call, so the
// "are there helper resolvers" check is cached behind an observer-maintained flag.
private ObservableList<IHelperResolver>? _observedResolvers;
private IObserver<IObservableEvent<IHelperResolver>>? _observerRoot; // strong root: ObservableList holds observers weakly

Check failure on line 13 in source/Handlebars/Helpers/BlockHelpers/LateBindBlockHelperDescriptor.cs

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Remove this unread private field '_observerRoot' or refactor the code to use its value.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_muU3fxezt7eMSJeXB&open=AZ_muU3fxezt7eMSJeXB&pullRequest=667
private volatile bool _hasResolvers;

public LateBindBlockHelperDescriptor(string name) => Name = name;

public PathInfo Name { get; }
Expand All@@ -16,16 +24,18 @@

public void Invoke(in EncodedTextWriter output, in BlockHelperOptions options, in Context context, in Arguments arguments)
{
if(options.Frame.BlockHelpers.TryGetValue(Name, out var contextHelper))
// Frame-local helpers only exist once something wrote to a frame's helper registry
// (decorators / in-render registration); skip the cascade walk in the common case.
if(options.Frame.HasFrameHelpers && options.Frame.BlockHelpers.TryGetValue(Name, out var contextHelper))
{
contextHelper.Invoke(options, context, arguments);
return;
}

// TODO: add cache

var configuration = options.Frame.Configuration;
var helperResolvers = (ObservableList<IHelperResolver>) configuration.HelperResolvers;
if(helperResolvers.Count != 0)
if (!ReferenceEquals(_observedResolvers, helperResolvers)) ObserveResolvers(helperResolvers);
if (_hasResolvers)
{
for (var index = 0; index < helperResolvers.Count; index++)
{
Expand All@@ -39,5 +49,19 @@
configuration.BlockHelpers["blockHelperMissing"]!.Value
.Invoke(output, options, context, arguments);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ObserveResolvers(ObservableList<IHelperResolver> resolvers)
{
// Subscribe before snapshotting Count so a concurrent Add can never be missed;
// duplicate subscriptions from a racing first call are benign (same flag).
var observer = ObserverBuilder<IObservableEvent<IHelperResolver>>.Create(this)
.OnEvent<AddedObservableEvent<IHelperResolver>>((_, state) => state._hasResolvers = true)
.Build();
resolvers.Subscribe(observer);
_observerRoot = observer;
if (resolvers.Count != 0) _hasResolvers = true;
_observedResolvers = resolvers;
}
}
}
}
37 changes: 31 additions & 6 deletions source/Handlebars/Helpers/LateBindHelperDescriptor.cs
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
using System;
using System.Runtime.CompilerServices;
using HandlebarsDotNet.Collections;
using HandlebarsDotNet.PathStructure;

namespace HandlebarsDotNet.Helpers
{
public sealed class LateBindHelperDescriptor : IHelperDescriptor<HelperOptions>
{
// ObservableList.Count takes a ReaderWriterLockSlim per call, and this descriptor runs
// for every simple {{name}} on every render — so the "are there helper resolvers" check
// is cached in a flag kept up to date by subscribing to the (append-only) resolver list.
private ObservableList<IHelperResolver>? _observedResolvers;
private IObserver<IObservableEvent<IHelperResolver>>? _observerRoot; // strong root: ObservableList holds observers weakly

Check failure on line 14 in source/Handlebars/Helpers/LateBindHelperDescriptor.cs

View check run for this annotation

SonarQubeCloud/ SonarCloud Code Analysis

Remove this unread private field '_observerRoot' or refactor the code to use its value.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_muU7Mxezt7eMSJeXC&open=AZ_muU7Mxezt7eMSJeXC&pullRequest=667
private volatile bool _hasResolvers;

public LateBindHelperDescriptor(string name) => Name = name;

public PathInfo Name { get; }
Expand All@@ -13,15 +22,17 @@
{
var bindingContext = options.Frame;

if(options.Frame.Helpers.TryGetValue(Name, out var contextHelper))
// Frame-local helpers only exist once something wrote to a frame's helper registry
// (decorators / in-render registration); skip the cascade walk in the common case.
if(bindingContext.HasFrameHelpers && bindingContext.Helpers.TryGetValue(Name, out var contextHelper))
{
return contextHelper.Invoke(options, context, arguments);
}

// TODO: add cache

var configuration = options.Frame.Configuration;
var helperResolvers = (ObservableList<IHelperResolver>) configuration.HelperResolvers;
if (helperResolvers.Count != 0)
if (!ReferenceEquals(_observedResolvers, helperResolvers)) ObserveResolvers(helperResolvers);
if (_hasResolvers)
{
var targetType = arguments.Length > 0 ? arguments[0]!.GetType() : null;
for (var index = 0; index < helperResolvers.Count; index++)
Expand All@@ -35,13 +46,27 @@

var value = PathResolver.ResolvePath(bindingContext, Name);
if (!(value is UndefinedBindingResult)) return value;

return configuration.Helpers["helperMissing"]!.Value.Invoke(options, context, arguments);
}

public void Invoke(in EncodedTextWriter output, in HelperOptions options, in Context context, in Arguments arguments)
{
output.Write(Invoke(options, context, arguments));
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void ObserveResolvers(ObservableList<IHelperResolver> resolvers)
{
// Subscribe before snapshotting Count so a concurrent Add can never be missed;
// duplicate subscriptions from a racing first call are benign (same flag).
var observer = ObserverBuilder<IObservableEvent<IHelperResolver>>.Create(this)
.OnEvent<AddedObservableEvent<IHelperResolver>>((_, state) => state._hasResolvers = true)
.Build();
resolvers.Subscribe(observer);
_observerRoot = observer;
if (resolvers.Count != 0) _hasResolvers = true;
_observedResolvers = resolvers;
}
}
}
}
6 changes: 5 additions & 1 deletion source/Handlebars/IO/PolledStringWriter.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,11 @@ namespace HandlebarsDotNet
{
public class ReusableStringWriter : StringWriter
{
private static readonly InternalObjectPool<ReusableStringWriter, Policy> Pool = new InternalObjectPool<ReusableStringWriter, Policy>(new Policy(16));
// Retain up to 32K chars (64KB, below the LOH threshold) so that templates producing
// typical page-sized output keep their grown StringBuilder across renders. The previous
// 4096-char limit made any render larger than 4KB discard the writer, forcing every
// subsequent render to re-grow a fresh StringBuilder(16) chunk by chunk.
private static readonly InternalObjectPool<ReusableStringWriter, Policy> Pool = new InternalObjectPool<ReusableStringWriter, Policy>(new Policy(16, 32 * 1024));

private IFormatProvider _formatProvider = null!;

Expand Down
14 changes: 13 additions & 1 deletion source/Handlebars/IO/SafeStrings.cs
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Threading;

namespace HandlebarsDotNet.IO
{
Expand All@@ -16,15 +17,26 @@ internal static class SafeStrings
private static readonly ConditionalWeakTable<string, object> Marked = new();
private static readonly object Sentinel = new();

// Marking only ever happens when a helper's captured output is written (ReturnInvoke).
// Most applications never mark a single string, yet IsSafe sits on the hot path of every
// string written to output — so keep a global "has anything ever been marked" latch to
// skip the ConditionalWeakTable probe entirely until the first Mark. The latch is written
// with release semantics after the table entry exists, so a true reader always observes
// the corresponding table entry; a stale false reader merely re-encodes on the same
// thread-interleaving that was already possible before the mark completed.
private static bool _anyMarked;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string Mark(string value)
{
if (value.Length == 0) return value;
Marked.GetValue(value, _ => Sentinel);
Volatile.Write(ref _anyMarked, true);
return value;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsSafe(string value) => value.Length == 0 || Marked.TryGetValue(value, out _);
public static bool IsSafe(string value)
=> value.Length == 0 || (Volatile.Read(ref _anyMarked) && Marked.TryGetValue(value, out _));
}
}
26 changes: 20 additions & 6 deletions source/Handlebars/ObjectDescriptors/ObjectDescriptorFactory.cs
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using HandlebarsDotNet.Collections;
using HandlebarsDotNet.EqualityComparers;
using HandlebarsDotNet.Runtime;
Expand All@@ -25,18 +26,31 @@ public class ObjectDescriptorFactory : IObjectDescriptorProvider, IObserver<IObs

private readonly IObserver<IObservableEvent<IObjectDescriptorProvider>> _observer;

private int _version;

public static ObjectDescriptorFactory? Current => AmbientContext.Current?.ObjectDescriptorFactory;


/// <summary>
/// Monotonic stamp bumped whenever the provider set changes; lets external
/// per-call-site descriptor caches (see <see cref="PathStructure.ChainSegment"/>)
/// detect that previously resolved descriptors may be stale.
/// </summary>
internal int Version => Volatile.Read(ref _version);

public ObjectDescriptorFactory(ObservableList<IObjectDescriptorProvider>? providers = null)
{
_providers = new ObservableList<IObjectDescriptorProvider>();

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

_observer = ObserverBuilder<IObservableEvent<IObjectDescriptorProvider>>.Create(_descriptorsCache)
.OnEvent<AddedObservableEvent<IObjectDescriptorProvider>>((@event, state) => state.Clear())

_observer = ObserverBuilder<IObservableEvent<IObjectDescriptorProvider>>.Create(this)
.OnEvent<AddedObservableEvent<IObjectDescriptorProvider>>((@event, state) =>
{
state._descriptorsCache.Clear();
Interlocked.Increment(ref state._version);
})
.Build();

_providers.Subscribe(this);
}

Expand Down
Loading
Loading