Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Several perf improvements around shape processing #15661

Merged
merged 10 commits into from
Apr 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ namespace OrchardCore.ContentTypes.Editors
public class DefaultContentDefinitionDisplayManager : BaseDisplayManager, IContentDefinitionDisplayManager
{
private readonly IEnumerable<IContentDefinitionDisplayHandler> _handlers;
private readonly IShapeTableManager _shapeTableManager;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IShapeFactory _shapeFactory;
private readonly ILayoutAccessor _layoutAccessor;
private readonly ILogger _logger;

public DefaultContentDefinitionDisplayManager(
IEnumerable<IContentDefinitionDisplayHandler> handlers,
IShapeTableManager shapeTableManager,
IContentDefinitionManager contentDefinitionManager,
IShapeFactory shapeFactory,
IEnumerable<IShapePlacementProvider> placementProviders,
Expand All @@ -33,7 +31,6 @@ ILayoutAccessor layoutAccessor
) : base(shapeFactory, placementProviders)
{
_handlers = handlers;
_shapeTableManager = shapeTableManager;
_contentDefinitionManager = contentDefinitionManager;
_shapeFactory = shapeFactory;
_layoutAccessor = layoutAccessor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public DynamicCacheShapeDisplayEvents(

public async Task DisplayingAsync(ShapeDisplayContext context)
{
if (!_cacheOptions.Enabled)
{
return;
}

// The shape has cache settings and no content yet.
if (context.Shape.Metadata.IsCached && context.ChildContent == null)
{
Expand All @@ -62,6 +67,11 @@ public async Task DisplayingAsync(ShapeDisplayContext context)

public async Task DisplayedAsync(ShapeDisplayContext context)
{
if (!_cacheOptions.Enabled)
{
return;
}

var cacheContext = context.Shape.Metadata.Cache();

// If the shape is not configured to be cached, continue as usual.
Expand Down Expand Up @@ -96,6 +106,11 @@ public async Task DisplayedAsync(ShapeDisplayContext context)

public Task DisplayingFinalizedAsync(ShapeDisplayContext context)
{
if (!_cacheOptions.Enabled)
{
return Task.CompletedTask;
}

var cacheContext = context.Shape.Metadata.Cache();

if (cacheContext != null && _openScopes.ContainsKey(cacheContext.CacheId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class AdminTemplatesShapeBindingResolver : IShapeBindingResolver
private readonly AdminPreviewTemplatesProvider _previewTemplatesProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly HtmlEncoder _htmlEncoder;
private bool? isAdmin;
sebastienros marked this conversation as resolved.
Show resolved Hide resolved

public AdminTemplatesShapeBindingResolver(
AdminTemplatesManager templatesManager,
Expand All @@ -34,7 +35,11 @@ public AdminTemplatesShapeBindingResolver(

public async Task<ShapeBinding> GetShapeBindingAsync(string shapeType)
{
if (!AdminAttribute.IsApplied(_httpContextAccessor.HttpContext))
// Cache this value since the service is scoped and this method is invoked for every
// alternate of every shape.
isAdmin ??= AdminAttribute.IsApplied(_httpContextAccessor.HttpContext);
sebastienros marked this conversation as resolved.
Show resolved Hide resolved

if (!isAdmin.Value)
sebastienros marked this conversation as resolved.
Show resolved Hide resolved
{
return null;
}
Expand Down Expand Up @@ -62,11 +67,7 @@ private ShapeBinding BuildShapeBinding(string shapeType, Template template)
{
BindingName = shapeType,
BindingSource = shapeType,
BindingAsync = async displayContext =>
{
var content = await _liquidTemplateManager.RenderHtmlContentAsync(template.Content, _htmlEncoder, displayContext.Value);
return content;
}
BindingAsync = displayContext => _liquidTemplateManager.RenderHtmlContentAsync(template.Content, _htmlEncoder, displayContext.Value)
};
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
Expand All @@ -12,11 +13,13 @@ namespace OrchardCore.Templates.Services
public class TemplatesShapeBindingResolver : IShapeBindingResolver
{
private TemplatesDocument _templatesDocument;
private readonly TemplatesDocument _localTemplates;

sebastienros marked this conversation as resolved.
Show resolved Hide resolved
private readonly TemplatesManager _templatesManager;
private readonly ILiquidTemplateManager _liquidTemplateManager;
private readonly PreviewTemplatesProvider _previewTemplatesProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly HtmlEncoder _htmlEncoder;
private bool? isAdmin;
sebastienros marked this conversation as resolved.
Show resolved Hide resolved

public TemplatesShapeBindingResolver(
TemplatesManager templatesManager,
Expand All @@ -27,23 +30,28 @@ public TemplatesShapeBindingResolver(
{
_templatesManager = templatesManager;
_liquidTemplateManager = liquidTemplateManager;
_previewTemplatesProvider = previewTemplatesProvider;
_httpContextAccessor = httpContextAccessor;
_htmlEncoder = htmlEncoder;
_localTemplates = previewTemplatesProvider.GetTemplates();
sebastienros marked this conversation as resolved.
Show resolved Hide resolved
}

public async Task<ShapeBinding> GetShapeBindingAsync(string shapeType)
{
if (AdminAttribute.IsApplied(_httpContextAccessor.HttpContext))
// Cache this value since the service is scoped and this method is invoked for every
// alternate of every shape.
isAdmin ??= AdminAttribute.IsApplied(_httpContextAccessor.HttpContext);
sebastienros marked this conversation as resolved.
Show resolved Hide resolved

if (isAdmin.Value)
sebastienros marked this conversation as resolved.
Show resolved Hide resolved
{
return null;
}

var localTemplates = _previewTemplatesProvider.GetTemplates();

if (localTemplates != null && localTemplates.Templates.TryGetValue(shapeType, out var localTemplate))
if (_localTemplates != null && _localTemplates.Templates.Any())
sebastienros marked this conversation as resolved.
Show resolved Hide resolved
{
return BuildShapeBinding(shapeType, localTemplate);
if (_localTemplates.Templates.TryGetValue(shapeType, out var localTemplate))
{
return BuildShapeBinding(shapeType, localTemplate);
}
}

_templatesDocument ??= await _templatesManager.GetTemplatesDocumentAsync();
Expand All @@ -62,11 +70,7 @@ private ShapeBinding BuildShapeBinding(string shapeType, Template template)
{
BindingName = shapeType,
BindingSource = shapeType,
BindingAsync = async displayContext =>
{
var content = await _liquidTemplateManager.RenderHtmlContentAsync(template.Content, _htmlEncoder, displayContext.Value);
return content;
}
BindingAsync = displayContext => _liquidTemplateManager.RenderHtmlContentAsync(template.Content, _htmlEncoder, displayContext.Value)
};
}
}
Expand Down
6 changes: 2 additions & 4 deletions src/OrchardCore/OrchardCore.DisplayManagement/IShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,8 @@ public static TagBuilder GetTagBuilder(this IShape shape, string defaultTagName

if (shape.Classes != null)
{
foreach (var cssClass in shape.Classes)
{
tagBuilder.AddCssClass(cssClass);
}
// Faster than AddCssClass which will do twice as many concatenations as classes.
tagBuilder.Attributes["class"] = string.Join(' ', shape.Classes);
sebastienros marked this conversation as resolved.
Show resolved Hide resolved
}

if (!string.IsNullOrWhiteSpace(shape.Id))
Expand Down
26 changes: 19 additions & 7 deletions src/OrchardCore/OrchardCore.DisplayManagement/IShapeFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Castle.DynamicProxy;
Expand All @@ -25,6 +27,7 @@ ValueTask<IShape> CreateAsync(

public static class ShapeFactoryExtensions
{
private static readonly ConcurrentDictionary<Type, Type> _proxyTypesCache = [];
private static readonly ProxyGenerator _proxyGenerator = new();
private static readonly Func<ValueTask<IShape>> _newShape = () => new(new Shape());

Expand All @@ -42,18 +45,27 @@ public static ValueTask<IShape> CreateAsync<TModel>(this IShapeFactory factory,

private static IShape CreateShape(Type baseType)
{
var shapeType = baseType;

// Don't generate a proxy for shape types
if (typeof(IShape).IsAssignableFrom(baseType))
if (typeof(IShape).IsAssignableFrom(shapeType))
{
var shape = Activator.CreateInstance(baseType) as IShape;
return shape;
return (IShape)Activator.CreateInstance(baseType);
}
else

if (_proxyTypesCache.TryGetValue(baseType, out var proxyType))
{
var options = new ProxyGenerationOptions();
options.AddMixinInstance(new ShapeViewModel());
return (IShape)_proxyGenerator.CreateClassProxy(baseType, options);
var model = new ShapeViewModel();
return (IShape)Activator.CreateInstance(proxyType, model, model, Array.Empty<IInterceptor>());
}

var options = new ProxyGenerationOptions();
options.AddMixinInstance(new ShapeViewModel());
var shape = (IShape)_proxyGenerator.CreateClassProxy(baseType, options);

_proxyTypesCache.TryAdd(baseType, shape.GetType());

return shape;
}

public static ValueTask<IShape> CreateAsync(this IShapeFactory factory, string shapeType)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
Expand All @@ -14,6 +15,7 @@ namespace OrchardCore.DisplayManagement.Implementation
public class DefaultHtmlDisplay : IHtmlDisplay
{
private const string _separator = "__";
private static ConcurrentDictionary<string, string[]> _alternateShapeTypes = [];

private readonly IShapeTableManager _shapeTableManager;
private readonly IEnumerable<IShapeDisplayEvents> _shapeDisplayEvents;
Expand Down Expand Up @@ -237,44 +239,50 @@ private async Task<ShapeBinding> GetShapeBindingAsync(string shapeType, Alternat
}

// When no alternates matches, the shapeType is used to find the longest matching binding,
// the shape-type name can break itself into shorter fallbacks at double-underscore marks,
// so the shape-type itself may contain a longer alternate forms that falls back to a shorter one.
var shapeTypeScan = shapeType;
// the shapetype name can break itself into shorter fallbacks at double-underscore marks,
// so the shapetype itself may contain a longer alternate forms that falls back to a shorter one.

do
// Build a cache of such values
var alternateShapeTypes = _alternateShapeTypes.GetOrAdd(shapeType, shapeType =>
sebastienros marked this conversation as resolved.
Show resolved Hide resolved
{
var segments = shapeType.Split("__");

if (segments.Length == 1)
{
return segments;
}

for (var i = 1; i < segments.Length; i++)
{
segments[i] = segments[i - 1] + "__" + segments[i];
}

Array.Reverse(segments);

return segments;
});

foreach (var shapeTypeSegment in alternateShapeTypes)
{
foreach (var shapeBindingResolver in _shapeBindingResolvers)
{
var binding = await shapeBindingResolver.GetShapeBindingAsync(shapeTypeScan);
var binding = await shapeBindingResolver.GetShapeBindingAsync(shapeTypeSegment);

if (binding != null)
{
return binding;
}
}

if (shapeTable.Bindings.TryGetValue(shapeTypeScan, out var shapeBinding))
if (shapeTable.Bindings.TryGetValue(shapeTypeSegment, out var shapeBinding))
{
return shapeBinding;
}
}
while (TryGetParentShapeTypeName(ref shapeTypeScan));

return null;
}

private static bool TryGetParentShapeTypeName(ref string shapeTypeScan)
{
var delimiterIndex = shapeTypeScan.LastIndexOf(_separator, StringComparison.Ordinal);
if (delimiterIndex > 0)
{
shapeTypeScan = shapeTypeScan[..delimiterIndex];
return true;
}

return false;
}

private static ValueTask<IHtmlContent> ProcessAsync(ShapeBinding shapeBinding, IShape shape, DisplayContext context)
{
static async ValueTask<IHtmlContent> Awaited(Task<IHtmlContent> task)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ public static OrchardCoreBuilder AddTheming(this OrchardCoreBuilder builder)
services.AddScoped<IUpdateModelAccessor, LocalModelBinderAccessor>();
services.AddScoped<ViewContextAccessor>();

services.AddScoped<IShapeTemplateViewEngine, RazorShapeTemplateViewEngine>();
services.AddScoped<RazorShapeTemplateViewEngine>();
services.AddScoped<IShapeTemplateViewEngine>(sp => sp.GetService<RazorShapeTemplateViewEngine>());

services.AddSingleton<IApplicationFeatureProvider<ViewsFeature>, ThemingViewsFeatureProvider>();
services.AddScoped<IViewLocationExpanderProvider, ThemeViewLocationExpanderProvider>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,37 @@ namespace OrchardCore.DisplayManagement.Razor
public class RazorShapeTemplateViewEngine : IShapeTemplateViewEngine
{
private readonly IOptions<MvcViewOptions> _options;
private readonly IEnumerable<IRazorViewExtensionProvider> _viewExtensionProviders;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ViewContextAccessor _viewContextAccessor;
private readonly ITempDataProvider _tempDataProvider;
private readonly List<string> _templateFileExtensions = new([RazorViewEngine.ViewExtension]);
private readonly IHtmlHelper _htmlHelper;

public RazorShapeTemplateViewEngine(
IOptions<MvcViewOptions> options,
IEnumerable<IRazorViewExtensionProvider> viewExtensionProviders,
IHttpContextAccessor httpContextAccessor,
ViewContextAccessor viewContextAccessor,
ITempDataProvider tempDataProvider)
ITempDataProvider tempDataProvider,
IHtmlHelper htmlHelper)
{
_options = options;
_viewExtensionProviders = viewExtensionProviders;
_httpContextAccessor = httpContextAccessor;
_viewContextAccessor = viewContextAccessor;
_tempDataProvider = tempDataProvider;
_templateFileExtensions.AddRange(viewExtensionProviders.Select(x => x.ViewExtension));
_htmlHelper = htmlHelper;
}

public IEnumerable<string> TemplateFileExtensions
{
get
{
return _templateFileExtensions;
yield return RazorViewEngine.ViewExtension;
foreach (var provider in _viewExtensionProviders)
{
yield return provider.ViewExtension;
}
}
}

Expand Down Expand Up @@ -164,18 +171,15 @@ private async Task<ActionContext> GetActionContextAsync()
return actionContext;
}

private static IHtmlHelper MakeHtmlHelper(ViewContext viewContext, ViewDataDictionary viewData)
private IHtmlHelper MakeHtmlHelper(ViewContext viewContext, ViewDataDictionary viewData)
{
var newHelper = viewContext.HttpContext.RequestServices.GetRequiredService<IHtmlHelper>();

var contextable = newHelper as IViewContextAware;
if (contextable != null)
if (_htmlHelper is IViewContextAware contextAwareHelper)
{
var newViewContext = new ViewContext(viewContext, viewContext.View, viewData, viewContext.Writer);
contextable.Contextualize(newViewContext);
contextAwareHelper.Contextualize(newViewContext);
}

return newHelper;
return _htmlHelper;
}
}
}
Loading
Loading