mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-04-27 03:53:10 -04:00
perf: add System.Text.Json source generators for hot-path serialization
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Text.Json;
|
||||
using System.Text;
|
||||
using allstarr.Models.Search;
|
||||
using allstarr.Models.Subsonic;
|
||||
using allstarr.Serialization;
|
||||
using allstarr.Services.Common;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -575,6 +576,20 @@ public partial class JellyfinController
|
||||
|
||||
private static string SerializeSearchResponseJson<T>(T response) where T : class
|
||||
{
|
||||
// Use source-gen context for registered types (no reflection, 3-8x faster)
|
||||
try
|
||||
{
|
||||
var typeInfo = AllstarrJsonContext.Shared.GetTypeInfo(typeof(T));
|
||||
if (typeInfo != null)
|
||||
{
|
||||
return JsonSerializer.Serialize(response, typeInfo);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Type not in source-gen context — fall through to reflection
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(response, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using allstarr.Models.Domain;
|
||||
using allstarr.Models.Lyrics;
|
||||
using allstarr.Models.Search;
|
||||
using allstarr.Models.Spotify;
|
||||
|
||||
namespace allstarr.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json source-generated serializer context for hot-path types.
|
||||
/// Eliminates runtime reflection for serialize/deserialize operations, providing
|
||||
/// 3-8x faster throughput and significantly reduced GC allocations.
|
||||
///
|
||||
/// Used by RedisCacheService (all cached types), search response serialization,
|
||||
/// and playback session payload construction.
|
||||
/// </summary>
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
)]
|
||||
// Domain models (hot: cached in Redis, serialized in search responses)
|
||||
[JsonSerializable(typeof(Song))]
|
||||
[JsonSerializable(typeof(Album))]
|
||||
[JsonSerializable(typeof(Artist))]
|
||||
[JsonSerializable(typeof(SearchResult))]
|
||||
[JsonSerializable(typeof(List<Song>))]
|
||||
[JsonSerializable(typeof(List<Album>))]
|
||||
[JsonSerializable(typeof(List<Artist>))]
|
||||
// Spotify models (hot: playlist loading, track matching)
|
||||
[JsonSerializable(typeof(SpotifyPlaylistTrack))]
|
||||
[JsonSerializable(typeof(SpotifyPlaylist))]
|
||||
[JsonSerializable(typeof(MatchedTrack))]
|
||||
[JsonSerializable(typeof(MissingTrack))]
|
||||
[JsonSerializable(typeof(SpotifyTrackMapping))]
|
||||
[JsonSerializable(typeof(TrackMetadata))]
|
||||
[JsonSerializable(typeof(List<SpotifyPlaylistTrack>))]
|
||||
[JsonSerializable(typeof(List<MatchedTrack>))]
|
||||
[JsonSerializable(typeof(List<MissingTrack>))]
|
||||
// Lyrics models (moderate: cached in Redis)
|
||||
[JsonSerializable(typeof(LyricsInfo))]
|
||||
// Collection types used in cache and playlist items
|
||||
[JsonSerializable(typeof(List<Dictionary<string, object?>>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object?>))]
|
||||
[JsonSerializable(typeof(List<string>))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(byte[]))]
|
||||
internal partial class AllstarrJsonContext : JsonSerializerContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared default instance. Use this for all hot-path serialization
|
||||
/// where PropertyNamingPolicy = null (PascalCase / preserve casing).
|
||||
/// </summary>
|
||||
public static AllstarrJsonContext Shared { get; } = new(new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = null,
|
||||
DictionaryKeyPolicy = null,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using allstarr.Models.Settings;
|
||||
using allstarr.Serialization;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace allstarr.Services.Common;
|
||||
|
||||
@@ -77,6 +79,8 @@ public class RedisCacheService
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached value and deserializes it.
|
||||
/// Uses source-generated serializer for registered types (3-8x faster),
|
||||
/// with automatic fallback to reflection-based serialization.
|
||||
/// </summary>
|
||||
public async Task<T?> GetAsync<T>(string key) where T : class
|
||||
{
|
||||
@@ -85,7 +89,11 @@ public class RedisCacheService
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
// Try source-gen context first (no reflection, much faster)
|
||||
var typeInfo = TryGetTypeInfo<T>();
|
||||
return typeInfo != null
|
||||
? JsonSerializer.Deserialize(json, typeInfo)
|
||||
: JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -197,12 +205,18 @@ public class RedisCacheService
|
||||
|
||||
/// <summary>
|
||||
/// Sets a cached value by serializing it with TTL.
|
||||
/// Uses source-generated serializer for registered types (3-8x faster),
|
||||
/// with automatic fallback to reflection-based serialization.
|
||||
/// </summary>
|
||||
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(value);
|
||||
// Try source-gen context first (no reflection, much faster)
|
||||
var typeInfo = TryGetTypeInfo<T>();
|
||||
var json = typeInfo != null
|
||||
? JsonSerializer.Serialize(value, typeInfo)
|
||||
: JsonSerializer.Serialize(value);
|
||||
return await SetStringAsync(key, json, expiry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -212,6 +226,22 @@ public class RedisCacheService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve a JsonTypeInfo from the AllstarrJsonContext source generator.
|
||||
/// Returns null if the type isn't registered, triggering fallback to reflection.
|
||||
/// </summary>
|
||||
private static JsonTypeInfo<T>? TryGetTypeInfo<T>() where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return (JsonTypeInfo<T>?)AllstarrJsonContext.Default.GetTypeInfo(typeof(T));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a cached value.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user