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
21 changes: 16 additions & 5 deletions source/Handlebars/Compiler/Translation/Expression/PathBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
{
internal class PathBinder : HandlebarsExpressionVisitor
{
private static readonly System.Reflection.MethodInfo WriteObjectToMethod =
typeof(EncodedTextWriter).GetMethod("WriteObjectTo",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)!;

Check warning on line 14 in source/Handlebars/Compiler/Translation/Expression/PathBinder.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure that this accessibility bypass is safe here.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_nLsJ13F1cw-NFBUm2&open=AZ_nLsJ13F1cw-NFBUm2&pullRequest=668

private CompilationContext CompilationContext { get; }

public PathBinder(CompilationContext compilationContext)
Expand Down Expand Up @@ -68,12 +72,20 @@
var contextValue = New(() => new Context(bindingContext));
var args = New(() => new Arguments(0));
var textWriter = CompilationContext.Args.EncodedWriter;
// Kept inline (unlike the plain-path statement route below): this is the
// hottest render-time shape, and routing it through a NoInlining entry point
// costs ~2ns per value per render. Compile cost of this shape is moderate
// because the interface dispatch below cannot be inline-expanded by the JIT.
return Call(() => helper.Value.Invoke(textWriter, options, contextValue, args));
}

var writer = CompilationContext.Args.EncodedWriter;
var value = Arg<object>(Visit(sex.Body));
return writer.Call(o => o.Write<object>(value));
// Emit as a static call with the writer in (in-)argument position: LambdaCompiler
// compiles an instance call on the struct parameter whose argument is a call
// result dramatically slower (~0.3ms+ per statement) than the equivalent
// static-call shape used by the helper invocation route.
return Expression.Call(WriteObjectToMethod, writer.Expression, value.Expression);
}

protected override Expression VisitPathExpression(PathExpression pex)
Expand Down Expand Up @@ -119,10 +131,9 @@
}
}

var options = New(() => new HelperOptions(pathInfo, bindingContext));
var context = New(() => new Context(bindingContext));
var argumentsArg = New(() => new Arguments(0));
return Call(() => helper.Value.Invoke(options, context, argumentsArg));
// Single NoInlining entry point instead of inline options/context/arguments
// construction + dispatch — see CompiledHelperInvokers for rationale.
return Call(() => CompiledHelperInvokers.Invoke(helper, pathInfo, bindingContext));
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions source/Handlebars/Helpers/CompiledHelperInvokers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Runtime.CompilerServices;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;

namespace HandlebarsDotNet.Helpers
{
/// <summary>
/// Static entry points emitted by the compiler for helper-literal statements
/// ({{name}} with no arguments). NoInlining keeps template JIT fast: dynamic methods
/// are compiled at CreateDelegate, and expanding the options/context/arguments
/// construction plus dispatch into every call site multiplied template JIT cost.
/// These methods are JIT-compiled once per process; templates emit one thin call.
/// </summary>
internal static class CompiledHelperInvokers
{
[MethodImpl(MethodImplOptions.NoInlining)]
internal static object? Invoke(
Ref<IHelperDescriptor<HelperOptions>> helper,
PathInfo pathInfo,
BindingContext bindingContext)
{
return helper.Value.Invoke(new HelperOptions(pathInfo, bindingContext), new Context(bindingContext), new Arguments(0));
}
}
}
8 changes: 8 additions & 0 deletions source/Handlebars/IO/EncodedTextWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
public TextWriter CreateWrapper() => EncodedTextWriterWrapper.From(this);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(string? value, bool encode)

Check warning on line 49 in source/Handlebars/IO/EncodedTextWriter.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

All 'Write' method overloads should be adjacent.

See more on https://sonarcloud.io/project/issues?id=Handlebars-Net_Handlebars.Net&issues=AZ_nLsMv3F1cw-NFBUm3&open=AZ_nLsMv3F1cw-NFBUm3&pullRequest=668
{
if(encode && !SuppressEncoding)
{
Expand Down Expand Up @@ -114,6 +114,14 @@
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(object? value) => Write<object>(value);

// Static entry point used by compiled templates ({{expr}} statements). NoInlining is
// what makes template compilation fast: dynamic methods are JIT-compiled at
// CreateDelegate, and inlining the aggressive-inline Write<object> type-switch and
// encoder machinery into every mustache call site multiplied template JIT cost ~6x.
// This wrapper is JIT-compiled once per process; templates emit one thin call.
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void WriteObjectTo(in EncodedTextWriter writer, object? value) => writer.Write<object>(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write<T>(T? value)
{
Expand Down
Loading