Skip to content

Commit

Permalink
Use Source Generation for json (de)serialization (#69)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfederm authored Apr 30, 2024
1 parent 67c92f7 commit 9aebe42
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 77 deletions.
4 changes: 2 additions & 2 deletions src/Common.Tests/NodeBuildResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ public void SortWorksConsistentlyAcrossJson()
null
);

string serialized = JsonSerializer.Serialize(nodeBuildResult);
NodeBuildResult deserialized = JsonSerializer.Deserialize<NodeBuildResult>(serialized)!;
string serialized = JsonSerializer.Serialize(nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult);
NodeBuildResult deserialized = JsonSerializer.Deserialize(serialized, SourceGenerationContext.Default.NodeBuildResult)!;

CollectionAssert.AreEqual(expected.Keys, deserialized.Outputs.Keys, "\n" +
"Permutation: " + string.Join(", ", permutation) + "\n" +
Expand Down
23 changes: 12 additions & 11 deletions src/Common/Caching/CacheClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
using BuildXL.Cache.ContentStore.Hashing;
Expand All @@ -21,7 +23,6 @@
using Microsoft.CopyOnWrite;
using Microsoft.MSBuildCache.Fingerprinting;
using Microsoft.MSBuildCache.Hashing;
using JsonSerializer = System.Text.Json.JsonSerializer;
using Fingerprint = Microsoft.MSBuildCache.Fingerprinting.Fingerprint;
using WeakFingerprint = BuildXL.Cache.MemoizationStore.Interfaces.Sessions.Fingerprint;

Expand Down Expand Up @@ -276,7 +277,7 @@ public async Task AddNodeInternalAsync(
Context context = new(RootContext);

// compute the metadata content.
byte[] nodeBuildResultBytes = await SerializeAsync(nodeBuildResult, cancellationToken);
byte[] nodeBuildResultBytes = await SerializeAsync(nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult, cancellationToken);
ContentHash nodeBuildResultHash = _hasher.GetContentHash(nodeBuildResultBytes)!;

Tracer.Debug(context, $"Computed node metadata {nodeBuildResultHash.ToShortString()} to the cache for {nodeContext.Id}");
Expand All @@ -286,7 +287,7 @@ public async Task AddNodeInternalAsync(
if (pathSet != null)
{
// Add the PathSet to the ContentStore
byte[] pathSetByteArray = await SerializeAsync(pathSet, cancellationToken);
byte[] pathSetByteArray = await SerializeAsync(pathSet, SourceGenerationContext.Default.PathSet, cancellationToken);
ContentHash pathSetBytesHash = _hasher.GetContentHash(pathSetByteArray)!;

Tracer.Debug(context, $"Computed PathSet {pathSetBytesHash.ToShortString()} to the cache for {nodeContext.Id}");
Expand Down Expand Up @@ -402,7 +403,7 @@ await AddNodeAsync(
}

// The first file is special: it is a serialized NodeBuildResult file.
NodeBuildResult? nodeBuildResult = await DeserializeAsync<NodeBuildResult>(context, nodeBuildResultStream, cancellationToken);
NodeBuildResult? nodeBuildResult = await DeserializeAsync(context, nodeBuildResultStream, SourceGenerationContext.Default.NodeBuildResult, cancellationToken);
if (nodeBuildResult is null)
{
Tracer.Debug(context, $"Failed to deserialize NodeBuildResult for {cacheStrongFingerprint}");
Expand Down Expand Up @@ -505,7 +506,7 @@ async Task PlaceFilesAsync(CancellationToken ct)
ContentHash pathSetHash = selector.ContentHash;
byte[]? selectorStrongFingerprint = selector.Output;

PathSet? pathSet = await FetchAndDeserializeFromCacheAsync<PathSet>(context, pathSetHash, cancellationToken);
PathSet? pathSet = await FetchAndDeserializeFromCacheAsync(context, pathSetHash, SourceGenerationContext.Default.PathSet, cancellationToken);

if (pathSet is null)
{
Expand All @@ -526,20 +527,20 @@ async Task PlaceFilesAsync(CancellationToken ct)
return (null, null);
}

private static async Task<byte[]> SerializeAsync<T>(T data, CancellationToken cancellationToken)
private static async Task<byte[]> SerializeAsync<T>(T data, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken)
where T : class
{
using (var memoryStream = new MemoryStream())
{
await JsonSerializer.SerializeAsync(memoryStream, data, SerializationHelper.SerializerOptions, cancellationToken);
await JsonSerializer.SerializeAsync(memoryStream, data, typeInfo, cancellationToken);
return memoryStream.ToArray();
}
}

private async Task<T?> DeserializeAsync<T>(Context context, Stream stream, CancellationToken cancellationToken)
private async Task<T?> DeserializeAsync<T>(Context context, Stream stream, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken)
where T : class
{
T? data = await stream.DeserializeAsync<T>(SerializationHelper.SerializerOptions, cancellationToken);
T? data = await stream.DeserializeAsync(typeInfo, cancellationToken);
if (data is null)
{
Tracer.Debug(context, $"Content deserialized as null");
Expand All @@ -548,7 +549,7 @@ private static async Task<byte[]> SerializeAsync<T>(T data, CancellationToken ca
return data;
}

protected async Task<T?> FetchAndDeserializeFromCacheAsync<T>(Context context, ContentHash contentHash, CancellationToken cancellationToken)
protected async Task<T?> FetchAndDeserializeFromCacheAsync<T>(Context context, ContentHash contentHash, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken)
where T : class
{
context = new(context);
Expand All @@ -562,7 +563,7 @@ private static async Task<byte[]> SerializeAsync<T>(T data, CancellationToken ca

using (streamResult.Stream)
{
return await DeserializeAsync<T>(context, streamResult.Stream!, cancellationToken);
return await DeserializeAsync(context, streamResult.Stream!, typeInfo, cancellationToken);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Common/MSBuildCachePluginBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ private static async Task DumpBuildResultLogAsync(
try
{
using FileStream fileStream = File.Create(filePath);
await JsonSerializer.SerializeAsync(fileStream, nodeBuildResult, SerializationHelper.SerializerOptions);
await JsonSerializer.SerializeAsync(fileStream, nodeBuildResult, SourceGenerationContext.Default.NodeBuildResult);
}
catch (Exception ex)
{
Expand Down
46 changes: 0 additions & 46 deletions src/Common/NodeBuildResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using BuildXL.Cache.ContentStore.Hashing;
using Microsoft.Build.Execution;
Expand Down Expand Up @@ -34,7 +33,6 @@ public NodeBuildResult(

// Use a sorted dictionary so the JSON output is deterministically sorted and easier to compare build-to-build.
// These paths are repo-relative.
[JsonConverter(typeof(SortedDictionaryConverter))]
public SortedDictionary<string, ContentHash> Outputs { get; }

// Use a sorted dictionary so the JSON output is deterministically sorted and easier to compare build-to-build.
Expand Down Expand Up @@ -76,48 +74,4 @@ public CacheResult ToCacheResult(PathNormalizer pathNormalizer)

return CacheResult.IndicateCacheHit(targetResults);
}

private sealed class SortedDictionaryConverter : JsonConverter<SortedDictionary<string, ContentHash>>
{
public override SortedDictionary<string, ContentHash>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var contentHashConverter = (JsonConverter<ContentHash>)options.GetConverter(typeof(ContentHash));
var outputs = new SortedDictionary<string, ContentHash>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}

if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException($"Unexpected token: {reader.TokenType}");
}

string propertyName = reader.GetString()!;
if (!reader.Read())
{
throw new JsonException($"Property name '{propertyName}' does not have a value.");
}

ContentHash? contentHash = contentHashConverter.Read(ref reader, typeof(ContentHash), options);
if (contentHash == null)
{
throw new JsonException($"Property value for '{propertyName}' could not be parsed.");
}

outputs.Add(propertyName, contentHash.Value);
}

return outputs;
}

public override void Write(Utf8JsonWriter writer, SortedDictionary<string, ContentHash> value, JsonSerializerOptions options)
{
var defaultConverter = (JsonConverter<SortedDictionary<string, ContentHash>>)
options.GetConverter(typeof(SortedDictionary<string, ContentHash>));
defaultConverter.Write(writer, value, options);
}
}
}
23 changes: 6 additions & 17 deletions src/Common/SerializationHelper.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.MSBuildCache;

internal static class SerializationHelper
{
public static JsonWriterOptions WriterOptions { get; } = new JsonWriterOptions { Indented = true };

public static JsonSerializerOptions SerializerOptions { get; } = CreateJsonSerializerOptions();

internal static async Task<T?> DeserializeAsync<T>(this Stream stream, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
internal static async Task<T?> DeserializeAsync<T>(this Stream stream, JsonTypeInfo<T> typeInfo, CancellationToken cancellationToken = default)
where T : class
{
try
{
return await JsonSerializer.DeserializeAsync<T>(stream, options, cancellationToken);
return await JsonSerializer.DeserializeAsync(stream, typeInfo, cancellationToken);
}
catch (JsonException)
{
Expand Down Expand Up @@ -56,14 +55,4 @@ internal static class SerializationHelper
throw new InvalidOperationException(message);
}
}

private static JsonSerializerOptions CreateJsonSerializerOptions()
{
JsonSerializerOptions options = new()
{
WriteIndented = true,
};
options.Converters.Add(new ContentHashJsonConverter());
return options;
}
}
53 changes: 53 additions & 0 deletions src/Common/SortedDictionaryConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using BuildXL.Cache.ContentStore.Hashing;
using System.Collections.Generic;

namespace Microsoft.MSBuildCache;

internal sealed class SortedDictionaryConverter : JsonConverter<SortedDictionary<string, ContentHash>>
{
public override SortedDictionary<string, ContentHash>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var contentHashConverter = (JsonConverter<ContentHash>)options.GetConverter(typeof(ContentHash));
var outputs = new SortedDictionary<string, ContentHash>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}

if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException($"Unexpected token: {reader.TokenType}");
}

string propertyName = reader.GetString()!;
if (!reader.Read())
{
throw new JsonException($"Property name '{propertyName}' does not have a value.");
}

ContentHash? contentHash = contentHashConverter.Read(ref reader, typeof(ContentHash), options);
if (contentHash == null)
{
throw new JsonException($"Property value for '{propertyName}' could not be parsed.");
}

outputs.Add(propertyName, contentHash.Value);
}

return outputs;
}

public override void Write(Utf8JsonWriter writer, SortedDictionary<string, ContentHash> value, JsonSerializerOptions options)
{
var defaultConverter = (JsonConverter<IDictionary<string, ContentHash>>)options.GetConverter(typeof(IDictionary<string, ContentHash>));
defaultConverter.Write(writer, value, options);
}
}
17 changes: 17 additions & 0 deletions src/Common/SourceGenerationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Text.Json.Serialization;
using BuildXL.Cache.ContentStore.Hashing;
using Microsoft.MSBuildCache.Fingerprinting;

namespace Microsoft.MSBuildCache;

[JsonSourceGenerationOptions(WriteIndented = true, Converters = [typeof(ContentHashJsonConverter), typeof(SortedDictionaryConverter)])]
[JsonSerializable(typeof(NodeBuildResult))]
[JsonSerializable(typeof(PathSet))]
[JsonSerializable(typeof(IDictionary<string, ContentHash>))]
internal partial class SourceGenerationContext : JsonSerializerContext
{
}

0 comments on commit 9aebe42

Please sign in to comment.