performance cache + queued character data application

This commit is contained in:
2025-12-05 10:49:30 +09:00
parent 1c36db97dc
commit 541d17132d
6 changed files with 324 additions and 8 deletions

View File

@@ -20,6 +20,7 @@ public sealed partial class PairCoordinator : MediatorSubscriberBase
private readonly PairManager _pairManager;
private readonly PairLedger _pairLedger;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly PairPerformanceMetricsCache _metricsCache;
private readonly ConcurrentDictionary<string, OnlineUserCharaDataDto> _pendingCharacterData = new(StringComparer.Ordinal);
public PairCoordinator(
@@ -29,7 +30,8 @@ public sealed partial class PairCoordinator : MediatorSubscriberBase
PairHandlerRegistry handlerRegistry,
PairManager pairManager,
PairLedger pairLedger,
ServerConfigurationManager serverConfigurationManager)
ServerConfigurationManager serverConfigurationManager,
PairPerformanceMetricsCache metricsCache)
: base(logger, mediator)
{
_logger = logger;
@@ -39,6 +41,7 @@ public sealed partial class PairCoordinator : MediatorSubscriberBase
_pairManager = pairManager;
_pairLedger = pairLedger;
_serverConfigurationManager = serverConfigurationManager;
_metricsCache = metricsCache;
mediator.Subscribe<ActiveServerChangedMessage>(this, msg => HandleActiveServerChange(msg.ServerUrl));
mediator.Subscribe<DisconnectedMessage>(this, _ => HandleDisconnected());
@@ -128,6 +131,7 @@ public sealed partial class PairCoordinator : MediatorSubscriberBase
_handlerRegistry.ResetAllHandlers();
_pairManager.ClearAll();
_pendingCharacterData.Clear();
_metricsCache.ClearAll();
_mediator.Publish(new ClearProfileUserDataMessage());
_mediator.Publish(new ClearProfileGroupDataMessage());
PublishPairDataChanged(groupChanged: true);

View File

@@ -41,6 +41,7 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
void Initialize();
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
@@ -67,6 +68,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly TextureDownscaleService _textureDownscaleService;
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
private readonly PairManager _pairManager;
private CancellationTokenSource? _applicationCancellationTokenSource = new();
private Guid _applicationId;
@@ -141,7 +143,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService,
PairStateCache pairStateCache) : base(logger, mediator)
PairStateCache pairStateCache,
PairPerformanceMetricsCache performanceMetricsCache) : base(logger, mediator)
{
_pairManager = pairManager;
Ident = ident;
@@ -157,6 +160,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_serverConfigManager = serverConfigManager;
_textureDownscaleService = textureDownscaleService;
_pairStateCache = pairStateCache;
_performanceMetricsCache = performanceMetricsCache;
LastAppliedDataBytes = -1;
}
@@ -493,7 +497,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
var sanitized = RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone());
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
if (sanitized is null)
{
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
@@ -513,6 +517,100 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ApplyCharacterData(Guid.NewGuid(), sanitized, shouldForce);
}
public bool FetchPerformanceMetricsFromCache()
{
EnsureInitialized();
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
if (sanitized is null || string.IsNullOrEmpty(dataHash))
{
return false;
}
if (!TryApplyCachedMetrics(dataHash))
{
return false;
}
_cachedData = sanitized;
_pairStateCache.Store(Ident, sanitized);
return true;
}
private CharacterData? CloneAndSanitizeLastReceived(out string? dataHash)
{
dataHash = null;
if (LastReceivedCharacterData is null)
{
return null;
}
var sanitized = RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone());
if (sanitized is null)
{
return null;
}
dataHash = GetDataHashSafe(sanitized);
return sanitized;
}
private string? GetDataHashSafe(CharacterData data)
{
try
{
return data.DataHash.Value;
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to compute character data hash for {Ident}", Ident);
return null;
}
}
private bool TryApplyCachedMetrics(string? dataHash)
{
if (string.IsNullOrEmpty(dataHash))
{
return false;
}
if (!_performanceMetricsCache.TryGetMetrics(Ident, dataHash, out var metrics))
{
return false;
}
ApplyCachedMetrics(metrics);
return true;
}
private void ApplyCachedMetrics(PairPerformanceMetrics metrics)
{
LastAppliedDataTris = metrics.TriangleCount;
LastAppliedApproximateVRAMBytes = metrics.ApproximateVramBytes;
LastAppliedApproximateEffectiveVRAMBytes = metrics.ApproximateEffectiveVramBytes;
}
private void StorePerformanceMetrics(CharacterData charaData)
{
if (LastAppliedDataTris < 0
|| LastAppliedApproximateVRAMBytes < 0
|| LastAppliedApproximateEffectiveVRAMBytes < 0)
{
return;
}
var dataHash = GetDataHashSafe(charaData);
if (string.IsNullOrEmpty(dataHash))
{
return;
}
_performanceMetricsCache.StoreMetrics(
Ident,
dataHash,
new PairPerformanceMetrics(LastAppliedDataTris, LastAppliedApproximateVRAMBytes, LastAppliedApproximateEffectiveVRAMBytes));
}
private bool HasMissingCachedFiles(CharacterData characterData)
{
try
@@ -878,6 +976,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_cachedData = null;
_lastAppliedModdedPaths = null;
_needsCollectionRebuild = false;
_performanceMetricsCache.Clear(Ident);
Logger.LogDebug("Disposing {name} complete", name);
}
}
@@ -1262,6 +1361,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
}
StorePerformanceMetrics(charaData);
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
}
catch (OperationCanceledException)
@@ -1693,6 +1793,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly ServerConfigurationManager _serverConfigManager;
private readonly TextureDownscaleService _textureDownscaleService;
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
public PairHandlerAdapterFactory(
ILoggerFactory loggerFactory,
@@ -1709,7 +1810,8 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService,
PairStateCache pairStateCache)
PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache)
{
_loggerFactory = loggerFactory;
_mediator = mediator;
@@ -1726,6 +1828,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_serverConfigManager = serverConfigManager;
_textureDownscaleService = textureDownscaleService;
_pairStateCache = pairStateCache;
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
}
public IPairHandlerAdapter Create(string ident)
@@ -1748,6 +1851,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_pairProcessingLimiter,
_serverConfigManager,
_textureDownscaleService,
_pairStateCache);
_pairStateCache,
_pairPerformanceMetricsCache);
}
}

View File

@@ -10,25 +10,32 @@ namespace LightlessSync.PlayerData.Pairs;
public sealed class PairHandlerRegistry : IDisposable
{
private readonly object _gate = new();
private readonly object _pendingGate = new();
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new();
private readonly IPairHandlerAdapterFactory _handlerFactory;
private readonly PairManager _pairManager;
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
private readonly ILogger<PairHandlerRegistry> _logger;
private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5);
private static readonly TimeSpan _handlerReadyTimeout = TimeSpan.FromMinutes(3);
private const int _handlerReadyPollDelayMs = 500;
private readonly Dictionary<string, CancellationTokenSource> _pendingCharacterData = new(StringComparer.Ordinal);
public PairHandlerRegistry(
IPairHandlerAdapterFactory handlerFactory,
PairManager pairManager,
PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache,
ILogger<PairHandlerRegistry> logger)
{
_handlerFactory = handlerFactory;
_pairManager = pairManager;
_pairStateCache = pairStateCache;
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
_logger = logger;
}
@@ -150,7 +157,8 @@ public sealed class PairHandlerRegistry : IDisposable
if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null)
{
return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}.");
QueuePendingCharacterData(registration, dto);
return PairOperationResult.Ok();
}
}
@@ -285,6 +293,8 @@ public sealed class PairHandlerRegistry : IDisposable
_entriesByHandler.Clear();
}
CancelAllPendingCharacterData();
foreach (var handler in handlers)
{
try
@@ -298,6 +308,10 @@ public sealed class PairHandlerRegistry : IDisposable
_logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident);
}
}
finally
{
_pairPerformanceMetricsCache.Clear(handler.Ident);
}
}
}
@@ -311,9 +325,12 @@ public sealed class PairHandlerRegistry : IDisposable
_entriesByHandler.Clear();
}
CancelAllPendingCharacterData();
foreach (var handler in handlers)
{
handler.Dispose();
_pairPerformanceMetricsCache.Clear(handler.Ident);
}
}
@@ -343,6 +360,7 @@ public sealed class PairHandlerRegistry : IDisposable
private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler)
{
string? ident = null;
lock (_gate)
{
if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs)
@@ -351,9 +369,126 @@ public sealed class PairHandlerRegistry : IDisposable
return false;
}
ident = entry.Ident;
_entriesByHandler.Remove(handler);
_entriesByIdent.Remove(entry.Ident);
return true;
}
if (ident is not null)
{
_pairPerformanceMetricsCache.Clear(ident);
CancelPendingCharacterData(ident);
}
return true;
}
private void QueuePendingCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
{
if (registration.CharacterIdent is null)
{
return;
}
CancellationTokenSource? previous = null;
CancellationTokenSource cts;
lock (_pendingGate)
{
if (_pendingCharacterData.TryGetValue(registration.CharacterIdent, out previous))
{
previous.Cancel();
}
cts = new CancellationTokenSource();
_pendingCharacterData[registration.CharacterIdent] = cts;
}
previous?.Dispose();
cts.CancelAfter(_handlerReadyTimeout);
_ = Task.Run(() => WaitThenApplyPendingCharacterDataAsync(registration, dto, cts.Token, cts));
}
private void CancelPendingCharacterData(string ident)
{
CancellationTokenSource? cts = null;
lock (_pendingGate)
{
if (_pendingCharacterData.TryGetValue(ident, out cts))
{
_pendingCharacterData.Remove(ident);
}
}
if (cts is not null)
{
cts.Cancel();
cts.Dispose();
}
}
private void CancelAllPendingCharacterData()
{
List<CancellationTokenSource>? snapshot = null;
lock (_pendingGate)
{
if (_pendingCharacterData.Count > 0)
{
snapshot = _pendingCharacterData.Values.ToList();
_pendingCharacterData.Clear();
}
}
if (snapshot is null)
{
return;
}
foreach (var cts in snapshot)
{
cts.Cancel();
cts.Dispose();
}
}
private async Task WaitThenApplyPendingCharacterDataAsync(
PairRegistration registration,
OnlineUserCharaDataDto dto,
CancellationToken token,
CancellationTokenSource source)
{
if (registration.CharacterIdent is null)
{
return;
}
try
{
while (!token.IsCancellationRequested)
{
if (TryGetHandler(registration.CharacterIdent, out var handler) && handler is not null && handler.Initialized)
{
handler.ApplyData(dto.CharaData);
break;
}
await Task.Delay(_handlerReadyPollDelayMs, token).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// expected
}
finally
{
lock (_pendingGate)
{
if (_pendingCharacterData.TryGetValue(registration.CharacterIdent, out var current) && ReferenceEquals(current, source))
{
_pendingCharacterData.Remove(registration.CharacterIdent);
}
}
source.Dispose();
}
}
}

View File

@@ -263,6 +263,11 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
continue;
}
if (handler.FetchPerformanceMetricsFromCache())
{
continue;
}
try
{
handler.ApplyLastReceivedData(forced: true);

View File

@@ -0,0 +1,65 @@
using System.Collections.Concurrent;
namespace LightlessSync.PlayerData.Pairs;
public readonly record struct PairPerformanceMetrics(
long TriangleCount,
long ApproximateVramBytes,
long ApproximateEffectiveVramBytes);
/// <summary>
/// caches performance metrics keyed by pair ident
/// </summary>
public sealed class PairPerformanceMetricsCache
{
private sealed record CacheEntry(string DataHash, PairPerformanceMetrics Metrics);
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new(StringComparer.Ordinal);
public bool TryGetMetrics(string ident, string dataHash, out PairPerformanceMetrics metrics)
{
metrics = default;
if (string.IsNullOrEmpty(ident) || string.IsNullOrEmpty(dataHash))
{
return false;
}
if (!_cache.TryGetValue(ident, out var entry))
{
return false;
}
if (!string.Equals(entry.DataHash, dataHash, StringComparison.Ordinal))
{
return false;
}
metrics = entry.Metrics;
return true;
}
public void StoreMetrics(string ident, string dataHash, PairPerformanceMetrics metrics)
{
if (string.IsNullOrEmpty(ident) || string.IsNullOrEmpty(dataHash))
{
return;
}
_cache[ident] = new CacheEntry(dataHash, metrics);
}
public void Clear(string ident)
{
if (string.IsNullOrEmpty(ident))
{
return;
}
_cache.TryRemove(ident, out _);
}
public void ClearAll()
{
_cache.Clear();
}
}

View File

@@ -174,11 +174,13 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<PlayerPerformanceConfigService>(), new Lazy<PairFactory>(() => s.GetRequiredService<PairFactory>())));
collection.AddSingleton<PairManager>();
collection.AddSingleton<PairStateCache>();
collection.AddSingleton<PairPerformanceMetricsCache>();
collection.AddSingleton<IPairHandlerAdapterFactory, PairHandlerAdapterFactory>();
collection.AddSingleton(s => new PairHandlerRegistry(
s.GetRequiredService<IPairHandlerAdapterFactory>(),
s.GetRequiredService<PairManager>(),
s.GetRequiredService<PairStateCache>(),
s.GetRequiredService<PairPerformanceMetricsCache>(),
s.GetRequiredService<ILogger<PairHandlerRegistry>>()));
collection.AddSingleton<PairLedger>();
collection.AddSingleton<PairUiService>();
@@ -201,7 +203,8 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<PairHandlerRegistry>(),
s.GetRequiredService<PairManager>(),
s.GetRequiredService<PairLedger>(),
s.GetRequiredService<ServerConfigurationManager>()));
s.GetRequiredService<ServerConfigurationManager>(),
s.GetRequiredService<PairPerformanceMetricsCache>()));
collection.AddSingleton<RedrawManager>();
collection.AddSingleton<LightFinderService>();
collection.AddSingleton(addonLifecycle);