perf(inventory): debounce LiveInventoryTracker update firehose
DECAL fires ChangeObject constantly for tracked items (mana-burn ticks, appraisal/ID data, stack tweaks); the update branch previously sent a full inventory_delta per event — a firehose across all characters that saturated the backend event loop (single worker pegged ~94% CPU, /live polls lagging, characters flickering in/out of the 30s online window). Now the update branch marks the item dirty and a System.Windows.Forms.Timer (main-thread, STA-safe) flushes the latest state of each dirty item once per 60s. Adds/removes stay immediate (rare and meaningful). Inventory view is up to 60s stale by design — invisible on a tracking dashboard. Built on the last-pushed base (placeholder secret, which works via the server's SHARED_SECRET_LEGACY); the websocket_secret.txt change is parked on branch secret-rollout for its own coordinated deploy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f72a555aef
commit
92a51b205c
2 changed files with 61 additions and 3 deletions
|
|
@ -15,9 +15,23 @@ namespace MosswartMassacre
|
|||
private readonly IPluginLogger _logger;
|
||||
private readonly HashSet<int> _trackedItemIds = new HashSet<int>();
|
||||
|
||||
// Items whose state changed since the last flush. DECAL fires
|
||||
// ChangeObject constantly for tracked items (mana-burn ticks, ID data
|
||||
// arriving, stack tweaks) — previously each one sent a full payload, a
|
||||
// firehose across all characters that saturated the backend. We now
|
||||
// coalesce those updates and flush the LATEST state of each dirty item
|
||||
// on a WinForms timer (game main thread → STA-safe for DECAL access),
|
||||
// so the backend sees ~1 update per item per flush instead of many per
|
||||
// second. Adds and removes stay immediate (they're rare and meaningful).
|
||||
private readonly HashSet<int> _dirtyItemIds = new HashSet<int>();
|
||||
private const int FlushIntervalMs = 60000; // 60s — tune here if needed
|
||||
private readonly System.Windows.Forms.Timer _flushTimer;
|
||||
|
||||
internal LiveInventoryTracker(IPluginLogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_flushTimer = new System.Windows.Forms.Timer { Interval = FlushIntervalMs };
|
||||
_flushTimer.Tick += FlushDirtyItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -27,6 +41,7 @@ namespace MosswartMassacre
|
|||
internal void Initialize()
|
||||
{
|
||||
_trackedItemIds.Clear();
|
||||
_dirtyItemIds.Clear();
|
||||
try
|
||||
{
|
||||
foreach (WorldObject wo in CoreManager.Current.WorldFilter.GetInventory())
|
||||
|
|
@ -38,6 +53,7 @@ namespace MosswartMassacre
|
|||
{
|
||||
_logger?.Log($"[LiveInv] Error initializing: {ex.Message}");
|
||||
}
|
||||
_flushTimer.Start();
|
||||
}
|
||||
|
||||
internal void OnCreateObject(object sender, CreateObjectEventArgs e)
|
||||
|
|
@ -111,9 +127,11 @@ namespace MosswartMassacre
|
|||
}
|
||||
else
|
||||
{
|
||||
// Existing item changed (equip/unequip, stack change, container move)
|
||||
var mwo = MyWorldObjectCreator.Create(item);
|
||||
_ = WebSocket.SendInventoryDeltaAsync("update", mwo);
|
||||
// Existing item changed (equip/unequip, stack change, mana
|
||||
// burn, ID data, container move). Coalesce: mark the item
|
||||
// dirty and let the flush timer send its latest state once,
|
||||
// instead of sending a full payload on every change.
|
||||
_dirtyItemIds.Add(item.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -122,8 +140,48 @@ namespace MosswartMassacre
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flush coalesced item updates: send the CURRENT state of each item
|
||||
/// that changed since the last tick, one "update" per item. Runs on the
|
||||
/// WinForms timer, i.e. the game's main thread, so the DECAL world-object
|
||||
/// access here is STA-safe.
|
||||
/// </summary>
|
||||
private void FlushDirtyItems(object sender, EventArgs e)
|
||||
{
|
||||
if (_dirtyItemIds.Count == 0) return;
|
||||
|
||||
// Snapshot then clear, so changes arriving during the flush are
|
||||
// captured for the next tick rather than dropped.
|
||||
var ids = new List<int>(_dirtyItemIds);
|
||||
_dirtyItemIds.Clear();
|
||||
|
||||
foreach (int id in ids)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Skip items that left inventory since being marked dirty —
|
||||
// the release/change-out path already sent a remove.
|
||||
if (!_trackedItemIds.Contains(id)) continue;
|
||||
|
||||
WorldObject item = CoreManager.Current.WorldFilter[id];
|
||||
if (item == null || !IsPlayerInventory(item)) continue;
|
||||
|
||||
var mwo = MyWorldObjectCreator.Create(item);
|
||||
_ = WebSocket.SendInventoryDeltaAsync("update", mwo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[LiveInv] Error flushing item {id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Cleanup()
|
||||
{
|
||||
// Stop the flush timer so the disposed/old instance can't keep
|
||||
// firing (and can be GC'd) across logout / hot reload.
|
||||
_flushTimer.Stop();
|
||||
_dirtyItemIds.Clear();
|
||||
_trackedItemIds.Clear();
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue