diff --git a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs deleted file mode 100644 index f16df01ef4..0000000000 --- a/src/Umbraco.Tests.Benchmarks/LocalizedTextServiceGetAllStoredValuesBenchmarks.cs +++ /dev/null @@ -1,561 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Loggers; -using Moq; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core.Services.Implement; -using System.Xml.Linq; -using Umbraco.Core.Logging; -using Umbraco.Tests.Benchmarks.Config; -using Umbraco.Core.Services; -using Umbraco.Core; -using System.Xml.XPath; -using ILogger = Umbraco.Core.Logging.ILogger; - -namespace Umbraco.Tests.Benchmarks -{ - [QuickRunWithMemoryDiagnoserConfig] - public class LocalizedTextServiceGetAllStoredValuesBenchmarks - { - private CultureInfo culture; - private OldLocalizedTextService _dictionaryService; - private OldLocalizedTextService _xmlService; - - private LocalizedTextService _optimized; - private LocalizedTextService _optimizedDict; - [GlobalSetup] - public void Setup() - { - culture = CultureInfo.GetCultureInfo("en-US"); - _dictionaryService = GetDictionaryLocalizedTextService(culture); - _xmlService = GetXmlService(culture); - _optimized = GetOptimizedService(culture); - _optimizedDict = GetOptimizedServiceDict(culture); - var result1 = _dictionaryService.Localize("language", culture); - var result2 = _xmlService.Localize("language", culture); - var result3 = _dictionaryService.GetAllStoredValues(culture); - var result4 = _xmlService.GetAllStoredValues(culture); - var result5 = _optimized.GetAllStoredValues(culture); - var result6 = _xmlService.GetAllStoredValues(culture); - var result7 = _optimized.GetAllStoredValuesByAreaAndAlias(culture); - } - - [Benchmark] - public void OriginalDictionaryGetAll() - { - for (int i = 0; i < 10000; i++) - { - var result = _dictionaryService.GetAllStoredValues(culture); - } - - } - - [Benchmark] - public void OriginalXmlGetAll() - { - for (int i = 0; i < 10000; i++) - { - var result = _xmlService.GetAllStoredValues(culture); - } - - } - - [Benchmark] - public void OriginalDictionaryLocalize() - { - for (int i = 0; i < 10000; i++) - { - var result = _dictionaryService.Localize("language", culture); - } - - } - - - [Benchmark(Baseline = true)] - public void OriginalXmlLocalize() - { - for (int i = 0; i < 10000; i++) - { - var result = _xmlService.Localize("language", culture); - } - } - [Benchmark] - public void OptimizedXmlGetAll() - { - for (int i = 0; i < 10000; i++) - { - var result = _optimized.GetAllStoredValues(culture); - } - - } - [Benchmark] - public void OptimizedDictGetAll() - { - for (int i = 0; i < 10000; i++) - { - var result = _optimizedDict.GetAllStoredValues(culture); - } - } - - [Benchmark] - public void OptimizedDictGetAllV2() - { - for (int i = 0; i < 10000; i++) - { - var result = _optimizedDict.GetAllStoredValuesByAreaAndAlias(culture); - } - } - - [Benchmark()] - public void OptimizedXmlLocalize() - { - for (int i = 0; i < 10000; i++) - { - var result = _optimized.Localize(null, "language", culture); - } - } - [Benchmark()] - public void OptimizedDictLocalize() - { - for (int i = 0; i < 10000; i++) - { - var result = _optimizedDict.Localize(null, "language", culture); - } - } - - private static LocalizedTextService GetOptimizedServiceDict(CultureInfo culture) - { - return new LocalizedTextService( - new Dictionary>> - { - { - culture, new Dictionary> - { - { - "testArea1", new Dictionary - { - {"testKey1", "testValue1"}, - {"testKey2", "testValue2"} - } - }, - { - "testArea2", new Dictionary - { - {"blah1", "blahValue1"}, - {"blah2", "blahValue2"} - } - }, - } - } - }, Mock.Of()); - } - private static LocalizedTextService GetOptimizedService(CultureInfo culture) - { - var txtService = new LocalizedTextService(new Dictionary> - { - { - culture, new Lazy(() => new XDocument( - new XElement("language", - new XElement("area", new XAttribute("alias", "testArea1"), - new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"), - new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")), - new XElement("area", new XAttribute("alias", "testArea2"), - new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"), - new XElement("key", new XAttribute("alias", "blah2"), "blahValue2"))))) - } - }, Mock.Of()); - return txtService; - } - - private static OldLocalizedTextService GetXmlService(CultureInfo culture) - { - var txtService = new OldLocalizedTextService(new Dictionary> - { - { - culture, new Lazy(() => new XDocument( - new XElement("language", - new XElement("area", new XAttribute("alias", "testArea1"), - new XElement("key", new XAttribute("alias", "testKey1"), "testValue1"), - new XElement("key", new XAttribute("alias", "testKey2"), "testValue2")), - new XElement("area", new XAttribute("alias", "testArea2"), - new XElement("key", new XAttribute("alias", "blah1"), "blahValue1"), - new XElement("key", new XAttribute("alias", "blah2"), "blahValue2"))))) - } - }, Mock.Of()); - return txtService; - } - - private static OldLocalizedTextService GetDictionaryLocalizedTextService(CultureInfo culture) - { - return new OldLocalizedTextService( - new Dictionary>> - { - { - culture, new Dictionary> - { - { - "testArea1", new Dictionary - { - {"testKey1", "testValue1"}, - {"testKey2", "testValue2"} - } - }, - { - "testArea2", new Dictionary - { - {"blah1", "blahValue1"}, - {"blah2", "blahValue2"} - } - }, - } - } - }, Mock.Of()); - } - } - - //Original - public class OldLocalizedTextService : ILocalizedTextService - { - private readonly ILogger _logger; - private readonly Lazy _fileSources; - private readonly IDictionary>> _dictionarySource; - private readonly IDictionary> _xmlSource; - - /// - /// Initializes with a file sources instance - /// - /// - /// - public OldLocalizedTextService(Lazy fileSources, ILogger logger) - { - if (logger == null) throw new ArgumentNullException("logger"); - _logger = logger; - if (fileSources == null) throw new ArgumentNullException("fileSources"); - _fileSources = fileSources; - } - - /// - /// Initializes with an XML source - /// - /// - /// - public OldLocalizedTextService(IDictionary> source, ILogger logger) - { - if (source == null) throw new ArgumentNullException("source"); - if (logger == null) throw new ArgumentNullException("logger"); - _xmlSource = source; - _logger = logger; - } - - /// - /// Initializes with a source of a dictionary of culture -> areas -> sub dictionary of keys/values - /// - /// - /// - public OldLocalizedTextService(IDictionary>> source, ILogger logger) - { - _dictionarySource = source ?? throw new ArgumentNullException(nameof(source)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public string Localize(string key, CultureInfo culture, IDictionary tokens = null) - { - if (culture == null) throw new ArgumentNullException(nameof(culture)); - - // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode - culture = ConvertToSupportedCultureWithRegionCode(culture); - - //This is what the legacy ui service did - if (string.IsNullOrEmpty(key)) - return string.Empty; - - var keyParts = key.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - var area = keyParts.Length > 1 ? keyParts[0] : null; - var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; - - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - if (xmlSource != null) - { - return GetFromXmlSource(xmlSource, culture, area, alias, tokens); - } - else - { - return GetFromDictionarySource(culture, area, alias, tokens); - } - } - - /// - /// Returns all key/values in storage for the given culture - /// - /// - public IDictionary GetAllStoredValues(CultureInfo culture) - { - if (culture == null) throw new ArgumentNullException("culture"); - - // TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode - culture = ConvertToSupportedCultureWithRegionCode(culture); - - var result = new Dictionary(); - - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - if (xmlSource != null) - { - if (xmlSource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; - } - - //convert all areas + keys to a single key with a '/' - result = GetStoredTranslations(xmlSource, culture); - - //merge with the English file in case there's keys in there that don't exist in the local file - var englishCulture = new CultureInfo("en-US"); - if (culture.Equals(englishCulture) == false) - { - var englishResults = GetStoredTranslations(xmlSource, englishCulture); - foreach (var englishResult in englishResults.Where(englishResult => result.ContainsKey(englishResult.Key) == false)) - result.Add(englishResult.Key, englishResult.Value); - } - } - else - { - if (_dictionarySource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return result; - } - - //convert all areas + keys to a single key with a '/' - foreach (var area in _dictionarySource[culture]) - { - foreach (var key in area.Value) - { - var dictionaryKey = string.Format("{0}/{1}", area.Key, key.Key); - //i don't think it's possible to have duplicates because we're dealing with a dictionary in the first place, but we'll double check here just in case. - if (result.ContainsKey(dictionaryKey) == false) - { - result.Add(dictionaryKey, key.Value); - } - } - } - } - - return result; - } - - private Dictionary GetStoredTranslations(IDictionary> xmlSource, CultureInfo cult) - { - var result = new Dictionary(); - var areas = xmlSource[cult].Value.XPathSelectElements("//area"); - foreach (var area in areas) - { - var keys = area.XPathSelectElements("./key"); - foreach (var key in keys) - { - var dictionaryKey = string.Format("{0}/{1}", (string)area.Attribute("alias"), - (string)key.Attribute("alias")); - //there could be duplicates if the language file isn't formatted nicely - which is probably the case for quite a few lang files - if (result.ContainsKey(dictionaryKey) == false) - result.Add(dictionaryKey, key.Value); - } - } - return result; - } - - /// - /// Returns a list of all currently supported cultures - /// - /// - public IEnumerable GetSupportedCultures() - { - var xmlSource = _xmlSource ?? (_fileSources != null - ? _fileSources.Value.GetXmlSources() - : null); - - return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; - } - - /// - /// Tries to resolve a full 4 letter culture from a 2 letter culture name - /// - /// - /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned - /// - /// - /// - /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that - /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts - /// to resolve the full culture if possible. - /// - /// This only works when this service is constructed with the LocalizedTextServiceFileSources - /// - public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture) - { - if (currentCulture == null) throw new ArgumentNullException("currentCulture"); - - if (_fileSources == null) return currentCulture; - if (currentCulture.Name.Length > 2) return currentCulture; - - var attempt = _fileSources.Value.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName); - return attempt ? attempt.Result : currentCulture; - } - - private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary tokens) - { - if (_dictionarySource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return "[" + key + "]"; - } - - var cultureSource = _dictionarySource[culture]; - - string found; - if (area.IsNullOrWhiteSpace()) - { - found = cultureSource - .SelectMany(x => x.Value) - .Where(keyvals => keyvals.Key.InvariantEquals(key)) - .Select(x => x.Value) - .FirstOrDefault(); - } - else - { - found = cultureSource - .Where(areas => areas.Key.InvariantEquals(area)) - .SelectMany(a => a.Value) - .Where(keyvals => keyvals.Key.InvariantEquals(key)) - .Select(x => x.Value) - .FirstOrDefault(); - } - - if (found != null) - { - return ParseTokens(found, tokens); - } - - //NOTE: Based on how legacy works, the default text does not contain the area, just the key - return "[" + key + "]"; - } - - private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) - { - if (xmlSource.ContainsKey(culture) == false) - { - _logger.Warn("The culture specified {Culture} was not found in any configured sources for this service", culture); - return "[" + key + "]"; - } - - var found = FindTranslation(xmlSource, culture, area, key); - - if (found != null) - { - return ParseTokens(found.Value, tokens); - } - - // Fall back to English by default if we can't find the key - found = FindTranslation(xmlSource, new CultureInfo("en-US"), area, key); - if (found != null) - return ParseTokens(found.Value, tokens); - - // If it can't be found in either file, fall back to the default, showing just the key in square brackets - // NOTE: Based on how legacy works, the default text does not contain the area, just the key - return "[" + key + "]"; - } - - private XElement FindTranslation(IDictionary> xmlSource, CultureInfo culture, string area, string key) - { - var cultureSource = xmlSource[culture].Value; - - var xpath = area.IsNullOrWhiteSpace() - ? string.Format("//key [@alias = '{0}']", key) - : string.Format("//area [@alias = '{0}']/key [@alias = '{1}']", area, key); - - var found = cultureSource.XPathSelectElement(xpath); - return found; - } - - /// - /// Parses the tokens in the value - /// - /// - /// - /// - /// - /// This is based on how the legacy ui localized text worked, each token was just a sequential value delimited with a % symbol. - /// For example: hello %0%, you are %1% ! - /// - /// Since we're going to continue using the same language files for now, the token system needs to remain the same. With our new service - /// we support a dictionary which means in the future we can really have any sort of token system. - /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case. - /// - internal static string ParseTokens(string value, IDictionary tokens) - { - if (tokens == null || tokens.Any() == false) - { - return value; - } - - foreach (var token in tokens) - { - value = value.Replace(string.Format("{0}{1}{0}", "%", token.Key), token.Value); - } - - return value; - } - - } - -// // * 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 | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | -//---------------------- |----------:|-----------:|----------:|------:|------------:|------------:|------------:|--------------------:| -// DictionaryGetAll | 11.199 ms | 1.8170 ms | 0.0996 ms | 0.14 | 1888.8889 | - | - | 5868.59 KB | -// XmlGetAll | 62.963 ms | 24.0615 ms | 1.3189 ms | 0.81 | 13500.0000 | - | - | 42448.71 KB | -// DictionaryLocalize | 9.757 ms | 1.6966 ms | 0.0930 ms | 0.13 | 1100.0000 | - | - | 3677.65 KB | -// XmlLocalize | 77.725 ms | 14.6069 ms | 0.8007 ms | 1.00 | 14000.0000 | - | - | 43032.8 KB | -// OptimizedXmlLocalize | 2.402 ms | 0.4256 ms | 0.0233 ms | 0.03 | 187.5000 | - | - | 626.01 KB | -// OptimizedDictLocalize | 2.345 ms | 0.2411 ms | 0.0132 ms | 0.03 | 187.5000 | - | - | 626.01 KB | - -//// * Warnings * -//MinIterationTime -// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryGetAll: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 99.7816 ms which is very small. It's recommended to increase it. -// LocalizedTextServiceGetAllStoredValuesBenchmarks.DictionaryLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 96.7415 ms which is very small. It's recommended to increase it. -// LocalizedTextServiceGetAllStoredValuesBenchmarks.XmlLocalize: IterationCount= 3, IterationTime= 100.0000 ms, LaunchCount= 1, WarmupCount= 3->MinIterationTime = 76.8151 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]) -// 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 ms : 1 Millisecond(0.001 sec) - -//// * Diagnostic Output - MemoryDiagnoser * - - -// // ***** BenchmarkRunner: End ***** -// Run time: 00:00:09 (9.15 sec), executed benchmarks: 6 -} diff --git a/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs index f54ab96255..a75e9db8bc 100644 --- a/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/ModelToSqlExpressionHelperBenchmarks.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq.Expressions; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Options; @@ -9,7 +9,6 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; -using Umbraco.Cms.Persistence.SqlCe; namespace Umbraco.Tests.Benchmarks { @@ -19,7 +18,7 @@ namespace Umbraco.Tests.Benchmarks protected Lazy MockSqlContext() { var sqlContext = Mock.Of(); - var syntax = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); + var syntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); Mock.Get(sqlContext).Setup(x => x.SqlSyntax).Returns(syntax); return new Lazy(() => sqlContext); } @@ -36,7 +35,7 @@ namespace Umbraco.Tests.Benchmarks _mapperCollection = mapperCollection.Object; } - private readonly ISqlSyntaxProvider _syntaxProvider = new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())); + private readonly ISqlSyntaxProvider _syntaxProvider = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); private readonly CachedExpression _cachedExpression; private readonly IMapperCollection _mapperCollection; diff --git a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs b/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs deleted file mode 100644 index 9f5a3c7453..0000000000 --- a/src/Umbraco.Tests.Benchmarks/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Umbraco.Tests.Benchmarks")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Umbraco.Tests.Benchmarks")] -[assembly: AssemblyCopyright("Copyright © 2018")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3a33adc9-c6c0-4db1-a613-a9af0210df3d")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs index 38167c1ff9..a69b6b8b76 100644 --- a/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs +++ b/src/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -1,4 +1,4 @@ -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using Microsoft.Extensions.Options; @@ -6,7 +6,7 @@ using NPoco; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; -using Umbraco.Cms.Persistence.SqlCe; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; namespace Umbraco.Tests.Benchmarks @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Benchmarks var mappers = new NPoco.MapperCollection( ); var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); - SqlContext = new SqlContext(new SqlCeSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); + SqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); SqlTemplates = new SqlTemplates(SqlContext); } diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index bb152a0bfd..2c1286172b 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -1,45 +1,12 @@ net5.0 - Exe - 8 + Exe false - - 7.3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -47,7 +14,6 @@ - @@ -59,11 +25,7 @@ 4.16.1 - - - all - + - - +