diff --git a/Solutions/Menes.Sandbox/Examples/JsonObjectExample.cs b/Solutions/Menes.Sandbox/Examples/JsonObjectExample.cs index b3c50ca6b..b7326ae36 100644 --- a/Solutions/Menes.Sandbox/Examples/JsonObjectExample.cs +++ b/Solutions/Menes.Sandbox/Examples/JsonObjectExample.cs @@ -11,22 +11,18 @@ namespace Examples { public static readonly Examples.JsonObjectExample Null = new Examples.JsonObjectExample(default); public static readonly System.Func FromJsonElement = e => new Examples.JsonObjectExample(e); - private const string FirstPropertyName = "first"; - private const string SecondPropertyName = "second"; - private const string ThirdPropertyName = "third"; - private const string ChildrenPropertyName = "children"; private const string FirstPropertyNamePath = ".first"; private const string SecondPropertyNamePath = ".second"; private const string ThirdPropertyNamePath = ".third"; private const string ChildrenPropertyNamePath = ".children"; - private static readonly System.Text.Json.JsonEncodedText EncodedFirstPropertyName = System.Text.Json.JsonEncodedText.Encode(FirstPropertyName); - private static readonly System.Text.Json.JsonEncodedText EncodedSecondPropertyName = System.Text.Json.JsonEncodedText.Encode(SecondPropertyName); - private static readonly System.Text.Json.JsonEncodedText EncodedThirdPropertyName = System.Text.Json.JsonEncodedText.Encode(ThirdPropertyName); - private static readonly System.Text.Json.JsonEncodedText EncodedChildrenPropertyName = System.Text.Json.JsonEncodedText.Encode(ChildrenPropertyName); - private static readonly System.ReadOnlyMemory FirstPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(FirstPropertyName); - private static readonly System.ReadOnlyMemory SecondPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(SecondPropertyName); - private static readonly System.ReadOnlyMemory ThirdPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(ThirdPropertyName); - private static readonly System.ReadOnlyMemory ChildrenPropertyNameBytes = System.Text.Encoding.UTF8.GetBytes(ChildrenPropertyName); + private static readonly System.ReadOnlyMemory FirstPropertyNameBytes = new byte[] { 102, 105, 114, 115, 116 }; + private static readonly System.ReadOnlyMemory SecondPropertyNameBytes = new byte[] { 115, 101, 99, 111, 110, 100 }; + private static readonly System.ReadOnlyMemory ThirdPropertyNameBytes = new byte[] { 116, 104, 105, 114, 100 }; + private static readonly System.ReadOnlyMemory ChildrenPropertyNameBytes = new byte[] { 99, 104, 105, 108, 100, 114, 101, 110 }; + private static readonly System.Text.Json.JsonEncodedText EncodedFirstPropertyName = System.Text.Json.JsonEncodedText.Encode(FirstPropertyNameBytes.Span); + private static readonly System.Text.Json.JsonEncodedText EncodedSecondPropertyName = System.Text.Json.JsonEncodedText.Encode(SecondPropertyNameBytes.Span); + private static readonly System.Text.Json.JsonEncodedText EncodedThirdPropertyName = System.Text.Json.JsonEncodedText.Encode(ThirdPropertyNameBytes.Span); + private static readonly System.Text.Json.JsonEncodedText EncodedChildrenPropertyName = System.Text.Json.JsonEncodedText.Encode(ChildrenPropertyNameBytes.Span); private static readonly System.Collections.Immutable.ImmutableArray> KnownProperties = System.Collections.Immutable.ImmutableArray.Create(FirstPropertyNameBytes, SecondPropertyNameBytes, ThirdPropertyNameBytes, ChildrenPropertyNameBytes); private readonly Menes.JsonString? first; private readonly Menes.JsonInt32? second; diff --git a/Solutions/Menes.TypeGenerator/Menes.TypeGenerator/ObjectTypeDeclaration.cs b/Solutions/Menes.TypeGenerator/Menes.TypeGenerator/ObjectTypeDeclaration.cs index e6c3302e3..d1d6aa099 100644 --- a/Solutions/Menes.TypeGenerator/Menes.TypeGenerator/ObjectTypeDeclaration.cs +++ b/Solutions/Menes.TypeGenerator/Menes.TypeGenerator/ObjectTypeDeclaration.cs @@ -985,10 +985,10 @@ private void BuildPropertyBackings(List members) { var propertyNames = new List<(string, string)>(); - this.BuildPropertyNameDeclarations(members, propertyNames); + this.BuildPropertyNameDeclarations(propertyNames); this.BuildPropertyNamePathDeclarations(propertyNames, members); - this.BuildEncodedPropertyNameDeclarations(propertyNames, members); this.BuildPropertyNameBytesDeclarations(propertyNames, members); + this.BuildEncodedPropertyNameDeclarations(propertyNames, members); this.AddKnownProperties(propertyNames, members); @@ -1003,12 +1003,11 @@ private void BuildPropertyBackingDeclarations(List memb } } - private void BuildPropertyNameDeclarations(List members, List<(string fieldName, string jsonPropertyName)> propertyNames) + private void BuildPropertyNameDeclarations(List<(string fieldName, string jsonPropertyName)> propertyNames) { foreach (PropertyDeclaration property in this.Properties) { string propertyNameFieldName = GetPropertyNameFieldName(property); - this.BuildPropertyNameDeclaration(propertyNameFieldName, property.JsonPropertyName, members); propertyNames.Add((propertyNameFieldName, property.JsonPropertyName)); } } @@ -1033,7 +1032,7 @@ private void BuildPropertyNameBytesDeclarations(List<(string, string)> propertyN { foreach ((string, string) propertyNameFieldName in propertyNameFieldNames) { - this.BuildPropertyNameBytesDeclaration(propertyNameFieldName.Item1, members); + this.BuildPropertyNameBytesDeclaration(propertyNameFieldName.Item1, propertyNameFieldName.Item2, members); } } @@ -1201,11 +1200,6 @@ private void BuildPropertyBackingDeclaration(PropertyDeclaration property, List< } } - private void BuildPropertyNameDeclaration(string propertyNameFieldName, string jsonPropertyName, List members) - { - members.Add(SF.ParseMemberDeclaration($"private const string {propertyNameFieldName} = \"{jsonPropertyName}\";" + Environment.NewLine)); - } - private void BuildPropertyNamePathDeclaration(string propertyNameFieldName, string jsonPropertyName, List members) { members.Add(SF.ParseMemberDeclaration($"private const string {propertyNameFieldName}Path = \".{jsonPropertyName}\";" + Environment.NewLine)); @@ -1213,12 +1207,29 @@ private void BuildPropertyNamePathDeclaration(string propertyNameFieldName, stri private void BuildEncodedPropertyNameDeclaration(string propertyNameFieldName, List members) { - members.Add(SF.ParseMemberDeclaration($"private static readonly System.Text.Json.JsonEncodedText Encoded{propertyNameFieldName} = System.Text.Json.JsonEncodedText.Encode({propertyNameFieldName});" + Environment.NewLine)); - } - - private void BuildPropertyNameBytesDeclaration(string propertyNameFieldName, List members) - { - members.Add(SF.ParseMemberDeclaration($"private static readonly System.ReadOnlyMemory {propertyNameFieldName}Bytes = System.Text.Encoding.UTF8.GetBytes({propertyNameFieldName});" + Environment.NewLine)); + members.Add(SF.ParseMemberDeclaration($"private static readonly System.Text.Json.JsonEncodedText Encoded{propertyNameFieldName} = System.Text.Json.JsonEncodedText.Encode({propertyNameFieldName}Bytes.Span);" + Environment.NewLine)); + } + + private void BuildPropertyNameBytesDeclaration(string propertyNameFieldName, string jsonPropertyName, List members) + { + // The C# compiler handles constant byte array initialization like this by embedding the binary + // data directly into the compiled assembly, and uses that to initialize the array directly. + // This results in better startup times than putting the call to Encoding.UTF8.GetBytes into the + // generated code itself. It means that we do the string processing here at code-gen time, instead + // of during static initialization. + // It's possible we could go a step further, because most of places that use this data obtain a + // ReadOnlySpan, and it turns out that the compiler can optimize the initialization of a span + // with a constant binary array even further: it doesn't need to allocate an array at all because + // it can produce a span that wraps the compiled byte stream directly. (Moreover, the code it + // generates to produce this span makes it possible for the JIT compiler to determine the span length, + // and there are scenarios where this can go on to improve performance by enabling the JIT to omit + // compile-time bounds checks. However, because of the limitations on span usage (it's a ref struct) + // we can't just make these properties return a ReadOnlySpan. In any case, there's currently one + // use of these properties that depends on hangs onto the Memory object: the list of all properties. + // It might be possible to create a more efficient formulation in which we have one great big binary + // array that contains all of the UTF8 data, but we'd need careful benchmarking to work out whether + // it did in fact produce an improvement. + members.Add(SF.ParseMemberDeclaration($"private static readonly System.ReadOnlyMemory {propertyNameFieldName}Bytes = new byte[] {{ {string.Join(", ", System.Text.Encoding.UTF8.GetBytes(jsonPropertyName).Select(b => b.ToString()))} }};" + Environment.NewLine)); } } }