removal of *temporary* collections

This commit is contained in:
2026-01-16 18:19:12 +09:00
parent 96123d00a2
commit e2d663cae9
11 changed files with 352 additions and 600 deletions

View File

@@ -1,10 +1,8 @@
using System.Collections.Concurrent;
using Dalamud.Plugin; using Dalamud.Plugin;
using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Penumbra.Api.Enums;
using Penumbra.Api.IpcSubscribers; using Penumbra.Api.IpcSubscribers;
namespace LightlessSync.Interop.Ipc.Penumbra; namespace LightlessSync.Interop.Ipc.Penumbra;
@@ -16,10 +14,6 @@ public sealed class PenumbraCollections : PenumbraBase
private readonly DeleteTemporaryCollection _removeTemporaryCollection; private readonly DeleteTemporaryCollection _removeTemporaryCollection;
private readonly AddTemporaryMod _addTemporaryMod; private readonly AddTemporaryMod _addTemporaryMod;
private readonly RemoveTemporaryMod _removeTemporaryMod; private readonly RemoveTemporaryMod _removeTemporaryMod;
private readonly GetCollections _getCollections;
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
private int _cleanupScheduled;
public PenumbraCollections( public PenumbraCollections(
ILogger logger, ILogger logger,
@@ -32,7 +26,6 @@ public sealed class PenumbraCollections : PenumbraBase
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface); _removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
_addTemporaryMod = new AddTemporaryMod(pluginInterface); _addTemporaryMod = new AddTemporaryMod(pluginInterface);
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface); _removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
_getCollections = new GetCollections(pluginInterface);
} }
public override string Name => "Penumbra.Collections"; public override string Name => "Penumbra.Collections";
@@ -62,16 +55,11 @@ public sealed class PenumbraCollections : PenumbraBase
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() => var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
{ {
var name = $"Lightless_{uid}"; var name = $"Lightless_{uid}";
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId); var createResult = _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId); logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}, Result: {Result}", name, tempCollectionId, createResult);
return (tempCollectionId, name); return (tempCollectionId, name);
}).ConfigureAwait(false); }).ConfigureAwait(false);
if (collectionId != Guid.Empty)
{
_activeTemporaryCollections[collectionId] = collectionName;
}
return collectionId; return collectionId;
} }
@@ -89,7 +77,6 @@ public sealed class PenumbraCollections : PenumbraBase
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result); logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
}).ConfigureAwait(false); }).ConfigureAwait(false);
_activeTemporaryCollections.TryRemove(collectionId, out _);
} }
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths) public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
@@ -131,67 +118,5 @@ public sealed class PenumbraCollections : PenumbraBase
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
{ {
if (current == IpcConnectionState.Available)
{
ScheduleCleanup();
}
else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available)
{
Interlocked.Exchange(ref _cleanupScheduled, 0);
}
} }
private void ScheduleCleanup()
{
if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0)
{
return;
}
_ = Task.Run(CleanupTemporaryCollectionsAsync);
}
private async Task CleanupTemporaryCollectionsAsync()
{
if (!IsAvailable)
{
return;
}
try
{
var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false);
foreach (var (collectionId, name) in collections)
{
if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId))
{
continue;
}
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
{
var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId);
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
return result;
}).ConfigureAwait(false);
if (deleteResult == PenumbraApiEc.Success)
{
_activeTemporaryCollections.TryRemove(collectionId, out _);
}
else
{
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
}
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections");
}
}
private static bool IsLightlessCollectionName(string? name)
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal);
} }

View File

@@ -4,6 +4,7 @@ using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using Penumbra.Api.Helpers; using Penumbra.Api.Helpers;
using Penumbra.Api.IpcSubscribers; using Penumbra.Api.IpcSubscribers;
@@ -42,17 +43,33 @@ public sealed class PenumbraResource : PenumbraBase
return null; return null;
} }
return await DalamudUtil.RunOnFrameworkThread(() => var requestId = Guid.NewGuid();
var totalTimer = Stopwatch.StartNew();
logger.LogTrace("[{requestId}] Requesting Penumbra.GetGameObjectResourcePaths for {handler}", requestId, handler);
var result = await DalamudUtil.RunOnFrameworkThread(() =>
{ {
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
var idx = handler.GetGameObject()?.ObjectIndex; var idx = handler.GetGameObject()?.ObjectIndex;
if (idx == null) if (idx == null)
{ {
logger.LogTrace("[{requestId}] GetGameObjectResourcePaths aborted (missing object index) for {handler}", requestId, handler);
return null; return null;
} }
return _gameObjectResourcePaths.Invoke(idx.Value)[0]; logger.LogTrace("[{requestId}] Invoking Penumbra.GetGameObjectResourcePaths for index {index}", requestId, idx.Value);
var invokeTimer = Stopwatch.StartNew();
var data = _gameObjectResourcePaths.Invoke(idx.Value)[0];
invokeTimer.Stop();
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths returned {count} entries in {elapsedMs}ms",
requestId, data?.Count ?? 0, invokeTimer.ElapsedMilliseconds);
return data;
}).ConfigureAwait(false); }).ConfigureAwait(false);
totalTimer.Stop();
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths finished in {elapsedMs}ms (null: {isNull})",
requestId, totalTimer.ElapsedMilliseconds, result is null);
return result;
} }
public string GetMetaManipulations() public string GetMetaManipulations()

View File

@@ -161,6 +161,7 @@ public class LightlessConfig : ILightlessConfiguration
public string LastSeenVersion { get; set; } = string.Empty; public string LastSeenVersion { get; set; } = string.Empty;
public bool EnableParticleEffects { get; set; } = true; public bool EnableParticleEffects { get; set; } = true;
public HashSet<Guid> OrphanableTempCollections { get; set; } = []; public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe; public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
public bool AnimationAllowOneBasedShift { get; set; } = true; public bool AnimationAllowOneBasedShift { get; set; } = true;

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.LightlessConfiguration.Models;
public sealed class OrphanableTempCollectionEntry
{
public Guid Id { get; set; }
public DateTime RegisteredAtUtc { get; set; } = DateTime.MinValue;
}

View File

@@ -257,7 +257,28 @@ public class PlayerDataFactory
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address); getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
} }
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character"); Guid penumbraRequestId = Guid.Empty;
Stopwatch? penumbraSw = null;
if (logDebug)
{
penumbraRequestId = Guid.NewGuid();
penumbraSw = Stopwatch.StartNew();
_logger.LogDebug("Penumbra GetCharacterData start {id} for {obj}", penumbraRequestId, playerRelatedObject);
}
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
if (logDebug)
{
penumbraSw!.Stop();
_logger.LogDebug("Penumbra GetCharacterData done {id} in {elapsedMs}ms (count={count})",
penumbraRequestId,
penumbraSw.ElapsedMilliseconds,
resolvedPaths?.Count ?? -1);
}
if (resolvedPaths == null)
throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct); var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);

View File

@@ -30,8 +30,6 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
int MissingCriticalMods { get; } int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; } int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; } int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
void Initialize(); void Initialize();
void ApplyData(CharacterData data); void ApplyData(CharacterData data);

View File

@@ -217,12 +217,6 @@ public class Pair
if (handler is null) if (handler is null)
return PairDebugInfo.Empty; return PairDebugInfo.Empty;
var now = DateTime.UtcNow;
var dueAt = handler.VisibilityEvictionDueAtUtc;
var remainingSeconds = dueAt.HasValue
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
: (double?)null;
return new PairDebugInfo( return new PairDebugInfo(
true, true,
handler.Initialized, handler.Initialized,
@@ -231,9 +225,6 @@ public class Pair
handler.LastDataReceivedAt, handler.LastDataReceivedAt,
handler.LastApplyAttemptAt, handler.LastApplyAttemptAt,
handler.LastSuccessfulApplyAt, handler.LastSuccessfulApplyAt,
handler.InvisibleSinceUtc,
handler.VisibilityEvictionDueAtUtc,
remainingSeconds,
handler.LastFailureReason, handler.LastFailureReason,
handler.LastBlockingConditions, handler.LastBlockingConditions,
handler.IsApplying, handler.IsApplying,

View File

@@ -8,9 +8,6 @@ public sealed record PairDebugInfo(
DateTime? LastDataReceivedAt, DateTime? LastDataReceivedAt,
DateTime? LastApplyAttemptAt, DateTime? LastApplyAttemptAt,
DateTime? LastSuccessfulApplyAt, DateTime? LastSuccessfulApplyAt,
DateTime? InvisibleSinceUtc,
DateTime? VisibilityEvictionDueAtUtc,
double? VisibilityEvictionRemainingSeconds,
string? LastFailureReason, string? LastFailureReason,
IReadOnlyList<string> BlockingConditions, IReadOnlyList<string> BlockingConditions,
bool IsApplying, bool IsApplying,
@@ -32,9 +29,6 @@ public sealed record PairDebugInfo(
null, null,
null, null,
null, null,
null,
null,
null,
Array.Empty<string>(), Array.Empty<string>(),
false, false,
false, false,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
using LightlessSync.Interop.Ipc; using System.Linq;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -10,6 +12,7 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
private readonly IpcManager _ipc; private readonly IpcManager _ipc;
private readonly LightlessConfigService _config; private readonly LightlessConfigService _config;
private int _ran; private int _ran;
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
public PenumbraTempCollectionJanitor( public PenumbraTempCollectionJanitor(
ILogger<PenumbraTempCollectionJanitor> logger, ILogger<PenumbraTempCollectionJanitor> logger,
@@ -26,15 +29,46 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
public void Register(Guid id) public void Register(Guid id)
{ {
if (id == Guid.Empty) return; if (id == Guid.Empty) return;
if (_config.Current.OrphanableTempCollections.Add(id)) var changed = false;
var config = _config.Current;
if (config.OrphanableTempCollections.Add(id))
{
changed = true;
}
var now = DateTime.UtcNow;
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
if (existing is null)
{
config.OrphanableTempCollectionEntries.Add(new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = now
});
changed = true;
}
else if (existing.RegisteredAtUtc == DateTime.MinValue)
{
existing.RegisteredAtUtc = now;
changed = true;
}
if (changed)
{
_config.Save(); _config.Save();
}
} }
public void Unregister(Guid id) public void Unregister(Guid id)
{ {
if (id == Guid.Empty) return; if (id == Guid.Empty) return;
if (_config.Current.OrphanableTempCollections.Remove(id)) var config = _config.Current;
var changed = config.OrphanableTempCollections.Remove(id);
changed |= RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
if (changed)
{
_config.Save(); _config.Save();
}
} }
private void CleanupOrphansOnBoot() private void CleanupOrphansOnBoot()
@@ -45,14 +79,33 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
if (!_ipc.Penumbra.APIAvailable) if (!_ipc.Penumbra.APIAvailable)
return; return;
var ids = _config.Current.OrphanableTempCollections.ToArray(); var config = _config.Current;
if (ids.Length == 0) var ids = config.OrphanableTempCollections;
var entries = config.OrphanableTempCollectionEntries;
if (ids.Count == 0 && entries.Count == 0)
return; return;
var appId = Guid.NewGuid(); var now = DateTime.UtcNow;
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length); var changed = EnsureEntries(ids, entries, now);
var cutoff = now - OrphanCleanupDelay;
var expired = entries
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
.Select(entry => entry.Id)
.Distinct()
.ToList();
if (expired.Count == 0)
{
if (changed)
{
_config.Save();
}
return;
}
foreach (var id in ids) var appId = Guid.NewGuid();
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
foreach (var id in expired)
{ {
try try
{ {
@@ -65,7 +118,70 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
} }
} }
_config.Current.OrphanableTempCollections.Clear(); foreach (var id in expired)
{
ids.Remove(id);
}
foreach (var id in expired)
{
RemoveEntry(entries, id);
}
_config.Save(); _config.Save();
} }
}
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
{
var removed = 0;
for (var i = entries.Count - 1; i >= 0; i--)
{
if (entries[i].Id != id)
{
continue;
}
entries.RemoveAt(i);
removed++;
}
return removed;
}
private static bool EnsureEntries(HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
{
var changed = false;
foreach (var id in ids)
{
if (id == Guid.Empty)
{
continue;
}
if (entries.Any(entry => entry.Id == id))
{
continue;
}
entries.Add(new OrphanableTempCollectionEntry
{
Id = id,
RegisteredAtUtc = now
});
changed = true;
}
foreach (var entry in entries)
{
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
{
continue;
}
entry.RegisteredAtUtc = now;
changed = true;
}
return changed;
}
}

View File

@@ -1485,8 +1485,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler)); DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized)); DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible)); DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion)); DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)"); DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
@@ -1622,8 +1620,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
sb.AppendLine($"Has Handler: {FormatBool(debugInfo.HasHandler)}"); sb.AppendLine($"Has Handler: {FormatBool(debugInfo.HasHandler)}");
sb.AppendLine($"Handler Initialized: {FormatBool(debugInfo.HandlerInitialized)}"); sb.AppendLine($"Handler Initialized: {FormatBool(debugInfo.HandlerInitialized)}");
sb.AppendLine($"Handler Visible: {FormatBool(debugInfo.HandlerVisible)}"); sb.AppendLine($"Handler Visible: {FormatBool(debugInfo.HandlerVisible)}");
sb.AppendLine($"Last Time person rendered in: {FormatTimestamp(debugInfo.InvisibleSinceUtc)}");
sb.AppendLine($"Handler Timer Temp Collection removal: {FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds)}");
sb.AppendLine($"Handler Scheduled For Deletion: {FormatBool(debugInfo.HandlerScheduledForDeletion)}"); sb.AppendLine($"Handler Scheduled For Deletion: {FormatBool(debugInfo.HandlerScheduledForDeletion)}");
sb.AppendLine($"Note: {pair.GetNote() ?? "(none)"}"); sb.AppendLine($"Note: {pair.GetNote() ?? "(none)"}");