Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Source Generation for json (de)serialization #69

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
{
}