Skip to content

Commit

Permalink
Several perf improvements around shape processing (#15661)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Apr 8, 2024
1 parent e1bde3d commit 835871c
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 57 deletions.
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;

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);

if (!_isAdmin.Value)
{
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;

private readonly TemplatesManager _templatesManager;
private readonly ILiquidTemplateManager _liquidTemplateManager;
private readonly PreviewTemplatesProvider _previewTemplatesProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly HtmlEncoder _htmlEncoder;
private bool? _isAdmin;

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();
}

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);

if (_isAdmin.Value)
{
return null;
}

var localTemplates = _previewTemplatesProvider.GetTemplates();

if (localTemplates != null && localTemplates.Templates.TryGetValue(shapeType, out var localTemplate))
if (_localTemplates?.Templates?.Count != 0)
{
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
10 changes: 4 additions & 6 deletions src/OrchardCore/OrchardCore.DisplayManagement/IShape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,15 @@ public static TagBuilder GetTagBuilder(this IShape shape, string defaultTagName

var tagBuilder = new TagBuilder(tagName);

if (shape.Attributes != null)
if (shape.Attributes?.Count > 0)
{
tagBuilder.MergeAttributes(shape.Attributes, false);
}

if (shape.Classes != null)
if (shape.Classes?.Count > 0)
{
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);
}

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,6 @@
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
Expand All @@ -14,6 +16,7 @@ namespace OrchardCore.DisplayManagement.Implementation
public class DefaultHtmlDisplay : IHtmlDisplay
{
private const string _separator = "__";
private static readonly ConcurrentDictionary<string, string[]> _alternateShapeTypes = [];

private readonly IShapeTableManager _shapeTableManager;
private readonly IEnumerable<IShapeDisplayEvents> _shapeDisplayEvents;
Expand Down Expand Up @@ -237,38 +240,53 @@ 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 =>
{
var segments = new List<string>(2);
var alternate = shapeType;
do
{
segments.Add(alternate);
} while (TryGetParentShapeTypeName(alternate, out alternate));
return segments.ToArray();
});

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)
private static bool TryGetParentShapeTypeName(string shapeTypeScan, out string parentType)
{
parentType = shapeTypeScan;

var delimiterIndex = shapeTypeScan.LastIndexOf(_separator, StringComparison.Ordinal);
if (delimiterIndex > 0)
{
shapeTypeScan = shapeTypeScan[..delimiterIndex];
parentType = shapeTypeScan[..delimiterIndex];
return true;
}

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

0 comments on commit 835871c

Please sign in to comment.