Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,11 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
||||
continue;
|
||||
}
|
||||
|
||||
if (handler.FetchPerformanceMetricsFromCache())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
handler.ApplyLastReceivedData(forced: true);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user