From 75ee3b96229d2fe5debfdf7fb274c321ae2342a8 Mon Sep 17 00:00:00 2001 From: Chad Date: Sun, 21 Feb 2021 23:00:00 +1300 Subject: [PATCH] Speed up boot times and Improve Json (De)Serialization performance and reduce memory usage by reusing JsonSerializerSettings (#9670) --- .../ImageCropperValueConverter.cs | 12 ++-- .../NoTypeConverterJsonConverter.cs | 5 +- .../JsonSerializerSettingsBenchmarks.cs | 69 +++++++++++++++++++ .../Umbraco.Tests.Benchmarks.csproj | 1 + .../Editors/BackOfficeController.cs | 6 +- .../ImageCropperTemplateExtensions.cs | 14 ++-- src/Umbraco.Web/Mvc/JsonNetResult.cs | 9 +++ .../MultiUrlPickerValueEditor.cs | 11 +-- .../NuCache/DataSource/DatabaseDataSource.cs | 11 +-- 9 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 8926174c03..29e501f993 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -25,6 +25,12 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; + /// public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { @@ -34,11 +40,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters ImageCropperValue value; try { - value = JsonConvert.DeserializeObject(sourceString, new JsonSerializerSettings - { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }); + value = JsonConvert.DeserializeObject(sourceString, ImageCropperValueJsonSerializerSettings); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs index b06ee870de..ab64d5b368 100644 --- a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs +++ b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Serialization internal class NoTypeConverterJsonConverter : JsonConverter { static readonly IContractResolver resolver = new NoTypeConverterContractResolver(); + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings { ContractResolver = resolver }; private class NoTypeConverterContractResolver : DefaultContractResolver { @@ -41,12 +42,12 @@ namespace Umbraco.Core.Serialization public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Deserialize(reader, objectType); + return JsonSerializer.CreateDefault(JsonSerializerSettings).Deserialize(reader, objectType); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { - JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Serialize(writer, value); + JsonSerializer.CreateDefault(JsonSerializerSettings).Serialize(writer, value); } } } diff --git a/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs new file mode 100644 index 0000000000..7f419547bd --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/JsonSerializerSettingsBenchmarks.cs @@ -0,0 +1,69 @@ +using BenchmarkDotNet.Attributes; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunConfig] + [MemoryDiagnoser] + public class JsonSerializerSettingsBenchmarks + { + [Benchmark] + public void SerializerSettingsInstantiation() + { + int instances = 1000; + for (int i = 0; i < instances; i++) + { + new JsonSerializerSettings(); + } + } + + [Benchmark(Baseline =true)] + public void SerializerSettingsSingleInstantiation() + { + new JsonSerializerSettings(); + } + +// // * Summary * + +// BenchmarkDotNet=v0.11.3, OS=Windows 10.0.18362 +//Intel Core i5-8265U CPU 1.60GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +// [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 +// Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.4250.0 + +//IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 +//WarmupCount=3 + +// Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | +//-------------------------------------- |-------------:|-------------:|------------:|-------:|--------:|------------:|------------:|------------:|--------------------:| +// SerializerSettingsInstantiation | 29,120.48 ns | 5,532.424 ns | 303.2508 ns | 997.84 | 23.66 | 73.8122 | - | - | 232346 B | +// SerializerSettingsSingleInstantiation | 29.19 ns | 8.089 ns | 0.4434 ns | 1.00 | 0.00 | 0.0738 | - | - | 232 B | + +//// * Warnings * +//MinIterationTime +// JsonSerializerSettingsBenchmarks.SerializerSettingsSingleInstantiation: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.2493 ms which is very small. It's recommended to increase it. + +//// * Legends * +// Mean : Arithmetic mean of all measurements +// Error : Half of 99.9% confidence interval +// StdDev : Standard deviation of all measurements +// Ratio : Mean of the ratio distribution ([Current]/[Baseline]) +// RatioSD : Standard deviation of the ratio distribution([Current]/[Baseline]) +// Gen 0/1k Op : GC Generation 0 collects per 1k Operations +// Gen 1/1k Op : GC Generation 1 collects per 1k Operations +// Gen 2/1k Op : GC Generation 2 collects per 1k Operations +// Allocated Memory/Op : Allocated memory per single operation(managed only, inclusive, 1KB = 1024B) +// 1 ns : 1 Nanosecond(0.000000001 sec) + +//// * Diagnostic Output - MemoryDiagnoser * + + +// // ***** BenchmarkRunner: End ***** +// Run time: 00:00:04 (4.88 sec), executed benchmarks: 2 + } +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 48d69cf757..58b45aa743 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -53,6 +53,7 @@ + diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 458c76b3ae..92b67cbf1b 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -226,7 +226,7 @@ namespace Umbraco.Web.Editors .ToDictionary(pv => pv.Key, pv => pv.ToDictionary(pve => pve.valueAlias, pve => pve.value)); - return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = nestedDictionary, Formatting = Formatting.None }; } /// @@ -273,7 +273,7 @@ namespace Umbraco.Web.Editors GetAssetList, new TimeSpan(0, 2, 0)); - return new JsonNetResult { Data = result, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = result, Formatting = Formatting.None }; } [UmbracoAuthorize(Order = 0)] @@ -281,7 +281,7 @@ namespace Umbraco.Web.Editors public JsonNetResult GetGridConfig() { var gridConfig = Current.Configs.Grids(); - return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; + return new JsonNetResult(JsonNetResult.DefaultJsonSerializerSettings) { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.None }; } diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 26dd2a5d36..78b55a8930 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -253,19 +253,20 @@ namespace Umbraco.Web ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + private static readonly JsonSerializerSettings ImageCropperValueJsonSerializerSettings = new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; internal static ImageCropperValue DeserializeImageCropperValue(this string json) { - var imageCrops = new ImageCropperValue(); + ImageCropperValue imageCrops = null; if (json.DetectIsJson()) { try { - imageCrops = JsonConvert.DeserializeObject(json, new JsonSerializerSettings - { - Culture = CultureInfo.InvariantCulture, - FloatParseHandling = FloatParseHandling.Decimal - }); + imageCrops = JsonConvert.DeserializeObject(json, ImageCropperValueJsonSerializerSettings); } catch (Exception ex) { @@ -273,6 +274,7 @@ namespace Umbraco.Web } } + imageCrops = imageCrops ?? new ImageCropperValue(); return imageCrops; } } diff --git a/src/Umbraco.Web/Mvc/JsonNetResult.cs b/src/Umbraco.Web/Mvc/JsonNetResult.cs index da6780451e..3dd6c2f398 100644 --- a/src/Umbraco.Web/Mvc/JsonNetResult.cs +++ b/src/Umbraco.Web/Mvc/JsonNetResult.cs @@ -22,10 +22,19 @@ namespace Umbraco.Web.Mvc public JsonSerializerSettings SerializerSettings { get; set; } public Formatting Formatting { get; set; } + /// + /// Default, unchanged JsonSerializerSettings + /// + public static readonly JsonSerializerSettings DefaultJsonSerializerSettings = new JsonSerializerSettings(); + public JsonNetResult() { SerializerSettings = new JsonSerializerSettings(); } + public JsonNetResult(JsonSerializerSettings jsonSerializerSettings) + { + SerializerSettings = jsonSerializerSettings; + } public override void ExecuteResult(ControllerContext context) { diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs index 5a84e4b20c..560275b29a 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -121,6 +121,10 @@ namespace Umbraco.Web.PropertyEditors return base.ToEditor(property, dataTypeService, culture, segment); } + private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }; public override object FromEditor(ContentPropertyData editorValue, object currentValue) { @@ -142,11 +146,8 @@ namespace Umbraco.Web.PropertyEditors Target = link.Target, Udi = link.Udi, Url = link.Udi == null ? link.Url : null, // only save the URL for external links - }, - new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }); + }, LinkDisplayJsonSerializerSettings + ); } catch (Exception ex) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index f62014a368..f9ad0ac715 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -303,17 +303,18 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource return s; } + private static readonly JsonSerializerSettings NestedContentDataJsonSerializerSettings = new JsonSerializerSettings + { + Converters = new List { new ForceInt32Converter() } + }; + private static ContentNestedData DeserializeNestedData(string data) { // by default JsonConvert will deserialize our numeric values as Int64 // which is bad, because they were Int32 in the database - take care - var settings = new JsonSerializerSettings - { - Converters = new List { new ForceInt32Converter() } - }; - return JsonConvert.DeserializeObject(data, settings); + return JsonConvert.DeserializeObject(data, NestedContentDataJsonSerializerSettings); } } }