From 541d17132dc5138bfdaec66d6c8e1d3208b10a1d Mon Sep 17 00:00:00 2001 From: azyges Date: Fri, 5 Dec 2025 10:49:30 +0900 Subject: [PATCH] performance cache + queued character data application --- .../PlayerData/Pairs/PairCoordinator.cs | 6 +- .../PlayerData/Pairs/PairHandlerAdapter.cs | 112 +++++++++++++- .../PlayerData/Pairs/PairHandlerRegistry.cs | 139 +++++++++++++++++- LightlessSync/PlayerData/Pairs/PairLedger.cs | 5 + .../Pairs/PairPerformanceMetricsCache.cs | 65 ++++++++ LightlessSync/Plugin.cs | 5 +- 6 files changed, 324 insertions(+), 8 deletions(-) create mode 100644 LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs diff --git a/LightlessSync/PlayerData/Pairs/PairCoordinator.cs b/LightlessSync/PlayerData/Pairs/PairCoordinator.cs index 7774851..3333eaa 100644 --- a/LightlessSync/PlayerData/Pairs/PairCoordinator.cs +++ b/LightlessSync/PlayerData/Pairs/PairCoordinator.cs @@ -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 _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(this, msg => HandleActiveServerChange(msg.ServerUrl)); mediator.Subscribe(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); diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index e1e58a6..70f4f0b 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -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); } } diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs index 97e3733..5421baa 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs @@ -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 _entriesByIdent = new(StringComparer.Ordinal); private readonly Dictionary _entriesByHandler = new(); private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly PairManager _pairManager; private readonly PairStateCache _pairStateCache; + private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; private readonly ILogger _logger; private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5); + private static readonly TimeSpan _handlerReadyTimeout = TimeSpan.FromMinutes(3); + private const int _handlerReadyPollDelayMs = 500; + private readonly Dictionary _pendingCharacterData = new(StringComparer.Ordinal); public PairHandlerRegistry( IPairHandlerAdapterFactory handlerFactory, PairManager pairManager, PairStateCache pairStateCache, + PairPerformanceMetricsCache pairPerformanceMetricsCache, ILogger 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? 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(); } } } diff --git a/LightlessSync/PlayerData/Pairs/PairLedger.cs b/LightlessSync/PlayerData/Pairs/PairLedger.cs index 66decfb..b151e1f 100644 --- a/LightlessSync/PlayerData/Pairs/PairLedger.cs +++ b/LightlessSync/PlayerData/Pairs/PairLedger.cs @@ -263,6 +263,11 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase continue; } + if (handler.FetchPerformanceMetricsFromCache()) + { + continue; + } + try { handler.ApplyLastReceivedData(forced: true); diff --git a/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs b/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs new file mode 100644 index 0000000..110d845 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairPerformanceMetricsCache.cs @@ -0,0 +1,65 @@ +using System.Collections.Concurrent; + +namespace LightlessSync.PlayerData.Pairs; + +public readonly record struct PairPerformanceMetrics( + long TriangleCount, + long ApproximateVramBytes, + long ApproximateEffectiveVramBytes); + +/// +/// caches performance metrics keyed by pair ident +/// +public sealed class PairPerformanceMetricsCache +{ + private sealed record CacheEntry(string DataHash, PairPerformanceMetrics Metrics); + + private readonly ConcurrentDictionary _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(); + } +} diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 41d8569..08065d1 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -174,11 +174,13 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), new Lazy(() => s.GetRequiredService()))); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(s => new PairHandlerRegistry( s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), + s.GetRequiredService(), s.GetRequiredService>())); collection.AddSingleton(); collection.AddSingleton(); @@ -201,7 +203,8 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService(), + s.GetRequiredService())); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(addonLifecycle);