Interns strings for aliases, etc... for when content is deserialized from the contentNu table so we aren't duplicating strings when cold booting.

This commit is contained in:
Shannon
2020-07-03 00:26:55 +10:00
parent d2042e28e1
commit e75c9d2273
11 changed files with 193 additions and 10 deletions

View File

@@ -0,0 +1,38 @@
using System;
using System.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Umbraco.Core.Serialization
{
/// <summary>
/// When applied to a string or string collection field will ensure the deserialized strings are interned
/// </summary>
/// <remarks>
/// Borrowed from https://stackoverflow.com/a/34906004/694494
/// On the same page an interesting approach of using a local intern pool https://stackoverflow.com/a/39605620/694494 which re-uses .NET System.Xml.NameTable
/// </remarks>
internal class AutoInterningStringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when a converter is applied directly to a property.
throw new NotImplementedException($"{nameof(AutoInterningStringConverter)} should not be used globally");
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
// Check is in case the value is a non-string literal such as an integer.
var s = reader.TokenType == JsonToken.String
? string.Intern((string)reader.Value)
: string.Intern((string)JToken.Load(reader));
return s;
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Umbraco.Core.Serialization
{
/// <summary>
/// When applied to a dictionary with a string key, will ensure the deserialized string keys are interned
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <remarks>
/// borrowed from https://stackoverflow.com/a/36116462/694494
/// </remarks>
internal class AutoInterningStringKeyCaseInsensitiveDictionaryConverter<TValue> : CaseInsensitiveDictionaryConverter<TValue>
{
public AutoInterningStringKeyCaseInsensitiveDictionaryConverter()
{
}
public AutoInterningStringKeyCaseInsensitiveDictionaryConverter(StringComparer comparer) : base(comparer)
{
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartObject)
{
var dictionary = new Dictionary<string, TValue>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
var key = string.Intern(reader.Value.ToString());
if (!reader.Read())
throw new Exception("Unexpected end when reading object.");
var v = serializer.Deserialize<TValue>(reader);
dictionary[key] = v;
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
return dictionary;
}
}
}
return null;
}
}
}

View File

@@ -14,12 +14,24 @@ namespace Umbraco.Core.Serialization
/// </example>
public class CaseInsensitiveDictionaryConverter<T> : CustomCreationConverter<IDictionary>
{
private readonly StringComparer _comparer;
public CaseInsensitiveDictionaryConverter()
: this(StringComparer.OrdinalIgnoreCase)
{
}
public CaseInsensitiveDictionaryConverter(StringComparer comparer)
{
_comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
}
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type objectType) => typeof(IDictionary<string,T>).IsAssignableFrom(objectType);
public override IDictionary Create(Type objectType) => new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
public override IDictionary Create(Type objectType) => new Dictionary<string, T>(_comparer);
}
}

View File

@@ -139,6 +139,8 @@
<Compile Include="Models\RelationTypeExtensions.cs" />
<Compile Include="Persistence\Repositories\IInstallationRepository.cs" />
<Compile Include="Persistence\Repositories\Implement\InstallationRepository.cs" />
<Compile Include="Serialization\AutoInterningStringConverter.cs" />
<Compile Include="Serialization\AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs" />
<Compile Include="Services\Implement\InstallationService.cs" />
<Compile Include="Migrations\Upgrade\V_8_6_0\AddMainDomLock.cs" />
<Compile Include="Models\UpgradeResult.cs" />

View File

@@ -0,0 +1,67 @@
using Newtonsoft.Json;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Umbraco.Core.Serialization;
namespace Umbraco.Tests.Serialization
{
[TestFixture]
public class AutoInterningStringConverterTests
{
[Test]
public void Intern_Property_String()
{
var str1 = "Hello";
var obj = new Test
{
Name = str1 + " " + "there"
};
// ensure the raw value is not interned
Assert.IsNull(string.IsInterned(obj.Name));
var serialized = JsonConvert.SerializeObject(obj);
obj = JsonConvert.DeserializeObject<Test>(serialized);
Assert.IsNotNull(string.IsInterned(obj.Name));
}
[Test]
public void Intern_Property_Dictionary()
{
var str1 = "key";
var obj = new Test
{
Values = new Dictionary<string, int>
{
[str1 + "1"] = 0,
[str1 + "2"] = 1
}
};
// ensure the raw value is not interned
Assert.IsNull(string.IsInterned(obj.Values.Keys.First()));
Assert.IsNull(string.IsInterned(obj.Values.Keys.Last()));
var serialized = JsonConvert.SerializeObject(obj);
obj = JsonConvert.DeserializeObject<Test>(serialized);
Assert.IsNotNull(string.IsInterned(obj.Values.Keys.First()));
Assert.IsNotNull(string.IsInterned(obj.Values.Keys.Last()));
}
public class Test
{
[JsonConverter(typeof(AutoInterningStringConverter))]
public string Name { get; set; }
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<int>))]
public Dictionary<string, int> Values = new Dictionary<string, int>();
}
}
}

View File

@@ -158,6 +158,7 @@
<Compile Include="Routing\RoutableDocumentFilterTests.cs" />
<Compile Include="Runtimes\StandaloneTests.cs" />
<Compile Include="Routing\GetContentUrlsTests.cs" />
<Compile Include="Serialization\AutoInterningStringConverterTests.cs" />
<Compile Include="Services\AmbiguousEventTests.cs" />
<Compile Include="Services\ContentServiceEventTests.cs" />
<Compile Include="Services\ContentServicePublishBranchTests.cs" />

View File

@@ -18,8 +18,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
var dict = new Dictionary<string, CultureVariation>(StringComparer.InvariantCultureIgnoreCase);
for (var i = 0; i < pcount; i++)
{
var languageId = PrimitiveSerializer.String.ReadFrom(stream);
var cultureVariation = new CultureVariation { Name = ReadStringObject(stream), UrlSegment = ReadStringObject(stream), Date = ReadDateTime(stream) };
var languageId = string.Intern(PrimitiveSerializer.String.ReadFrom(stream));
var cultureVariation = new CultureVariation
{
Name = ReadStringObject(stream),
UrlSegment = ReadStringObject(stream),
Date = ReadDateTime(stream)
};
dict[languageId] = cultureVariation;
}
return dict;

View File

@@ -38,8 +38,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
// the 'current' value, and string.Empty should be used to represent the invariant or
// neutral values - PropertyData throws when getting nulls, so falling back to
// string.Empty here - what else?
pdata.Culture = string.Intern(ReadStringObject(stream)) ?? string.Empty;
pdata.Segment = string.Intern(ReadStringObject(stream)) ?? string.Empty;
pdata.Culture = ReadStringObject(stream, true) ?? string.Empty;
pdata.Segment = ReadStringObject(stream, true) ?? string.Empty;
pdata.Value = ReadObject(stream);
}

View File

@@ -11,11 +11,11 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
{
//dont serialize empty properties
[JsonProperty("pd")]
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<PropertyData[]>))]
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<PropertyData[]>))]
public Dictionary<string, PropertyData[]> PropertyData { get; set; }
[JsonProperty("cd")]
[JsonConverter(typeof(CaseInsensitiveDictionaryConverter<CultureVariation>))]
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter<CultureVariation>))]
public Dictionary<string, CultureVariation> CultureData { get; set; }
[JsonProperty("us")]

View File

@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using Newtonsoft.Json;
using Umbraco.Core.Serialization;
namespace Umbraco.Web.PublishedCache.NuCache.DataSource
{
@@ -9,6 +10,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
private string _culture;
private string _segment;
[JsonConverter(typeof(AutoInterningStringConverter))]
[DefaultValue("")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")]
public string Culture
@@ -17,6 +19,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null
}
[JsonConverter(typeof(AutoInterningStringConverter))]
[DefaultValue("")]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")]
public string Segment
@@ -28,7 +31,6 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
[JsonProperty("v")]
public object Value { get; set; }
//Legacy properties used to deserialize existing nucache db entries
[JsonProperty("culture")]
private string LegacyCulture

View File

@@ -23,13 +23,15 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
return read(stream);
}
protected string ReadStringObject(Stream stream) // required 'cos string is not a struct
protected string ReadStringObject(Stream stream, bool intern = false) // required 'cos string is not a struct
{
var type = PrimitiveSerializer.Char.ReadFrom(stream);
if (type == 'N') return null;
if (type != 'S')
throw new NotSupportedException($"Cannot deserialize type '{type}', expected 'S'.");
return PrimitiveSerializer.String.ReadFrom(stream);
return intern
? string.Intern(PrimitiveSerializer.String.ReadFrom(stream))
: PrimitiveSerializer.String.ReadFrom(stream);
}
protected int? ReadIntObject(Stream stream) => ReadObject(stream, 'I', ReadInt);