Make DtrBar more threadsafe (#1978)

* Changed DtrBar to use ReaderWriterLockSlim so that there exists only one storage of entries, preventing possible desync.
* DtrBarEntry will now hold a reference to the LocalPlugin that created the entry, so that DtrBarPluginScoped can defer plugin related handling to the main service.
* Marked DtrBarEntry class itself to be turned internal in API 11.
* Made IDtrBar.Entries return an immutable copy of underlying list of DtrBar entries, that will be freshly created whenever the list changes.
This commit is contained in:
srkizer 2024-07-28 21:14:37 +09:00 committed by GitHub
parent a7ab3b9def
commit c25f13261d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 385 additions and 207 deletions

View file

@ -1,4 +1,8 @@
using Dalamud.Configuration.Internal;
using System.Linq;
using System.Threading;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;
using ImGuiNET;
@ -8,17 +12,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
/// <summary>
/// Widget for displaying dtr test.
/// </summary>
internal class DtrBarWidget : IDataWindowWidget
internal class DtrBarWidget : IDataWindowWidget, IDisposable
{
private IDtrBarEntry? dtrTest1;
private IDtrBarEntry? dtrTest2;
private IDtrBarEntry? dtrTest3;
private Thread? loadTestThread;
private CancellationTokenSource? loadTestThreadCt;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" };
/// <inheritdoc/>
public string DisplayName { get; init; } = "DTR Bar";
public string DisplayName { get; init; } = "DTR Bar";
/// <inheritdoc/>
public bool Ready { get; set; }
@ -26,31 +33,145 @@ internal class DtrBarWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.ClearState();
this.Ready = true;
}
/// <inheritdoc/>
public void Dispose() => this.ClearState();
/// <inheritdoc/>
public void Draw()
{
if (this.loadTestThread?.IsAlive is not true)
{
if (ImGui.Button("Do multithreaded add/remove operation"))
{
var ct = this.loadTestThreadCt = new();
var dbar = Service<DtrBar>.Get();
var fw = Service<Framework>.Get();
var rng = new Random();
this.loadTestThread = new(
() =>
{
var threads = Enumerable
.Range(0, Environment.ProcessorCount)
.Select(
i => new Thread(
(i % 4) switch
{
0 => () =>
{
try
{
while (true)
{
var n = $"DtrBarWidgetTest{rng.NextInt64(8)}";
dbar.Get(n, n[^5..]);
fw.DelayTicks(1, ct.Token).Wait(ct.Token);
ct.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
// ignore
}
},
1 => () =>
{
try
{
while (true)
{
dbar.Remove($"DtrBarWidgetTest{rng.NextInt64(8)}");
fw.DelayTicks(1, ct.Token).Wait(ct.Token);
ct.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
// ignore
}
},
2 => () =>
{
try
{
while (true)
{
var n = $"DtrBarWidgetTest{rng.NextInt64(8)}_";
dbar.Get(n, n[^6..]);
ct.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
// ignore
}
},
_ => () =>
{
try
{
while (true)
{
dbar.Remove($"DtrBarWidgetTest{rng.NextInt64(8)}_");
ct.Token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
// ignore
}
},
}))
.ToArray();
foreach (var t in threads) t.Start();
foreach (var t in threads) t.Join();
for (var i = 0; i < 8; i++) dbar.Remove($"DtrBarWidgetTest{i % 8}");
for (var i = 0; i < 8; i++) dbar.Remove($"DtrBarWidgetTest{i % 8}_");
});
this.loadTestThread.Start();
}
}
else
{
if (ImGui.Button("Stop multithreaded add/remove operation"))
this.ClearState();
}
ImGui.Separator();
this.DrawDtrTestEntry(ref this.dtrTest1, "DTR Test #1");
ImGui.Separator();
this.DrawDtrTestEntry(ref this.dtrTest2, "DTR Test #2");
ImGui.Separator();
this.DrawDtrTestEntry(ref this.dtrTest3, "DTR Test #3");
ImGui.Separator();
ImGui.Text("IDtrBar.Entries:");
foreach (var e in Service<DtrBar>.Get().Entries)
ImGui.Text(e.Title);
var configuration = Service<DalamudConfiguration>.Get();
if (configuration.DtrOrder != null)
{
ImGui.Separator();
ImGui.Text("DtrOrder:");
foreach (var order in configuration.DtrOrder)
{
ImGui.Text(order);
}
}
}
private void ClearState()
{
this.loadTestThreadCt?.Cancel();
this.loadTestThread?.Join();
this.loadTestThread = null;
this.loadTestThreadCt = null;
}
private void DrawDtrTestEntry(ref IDtrBarEntry? entry, string title)
{
var dtrBar = Service<DtrBar>.Get();

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using CheapLoc;
using Dalamud.Configuration.Internal;
@ -45,6 +46,10 @@ public class SettingsTabDtr : SettingsTab
}
var isOrderChange = false;
Span<Vector2> upButtonCenters = stackalloc Vector2[order.Count];
Span<Vector2> downButtonCenters = stackalloc Vector2[order.Count];
scoped Span<Vector2> moveMouseTo = default;
var moveMouseToIndex = -1;
for (var i = 0; i < order.Count; i++)
{
var title = order[i];
@ -65,9 +70,13 @@ public class SettingsTabDtr : SettingsTab
{
(order[i], order[i - 1]) = (order[i - 1], order[i]);
isOrderChange = true;
moveMouseToIndex = i - 1;
moveMouseTo = upButtonCenters;
}
}
upButtonCenters[i] = (ImGui.GetItemRectMin() + ImGui.GetItemRectMax()) / 2;
ImGui.SameLine();
var arrowDownText = $"{FontAwesomeIcon.ArrowDown.ToIconString()}##{title}";
@ -81,9 +90,13 @@ public class SettingsTabDtr : SettingsTab
{
(order[i], order[i + 1]) = (order[i + 1], order[i]);
isOrderChange = true;
moveMouseToIndex = i + 1;
moveMouseTo = downButtonCenters;
}
}
downButtonCenters[i] = (ImGui.GetItemRectMin() + ImGui.GetItemRectMax()) / 2;
ImGui.PopFont();
ImGui.SameLine();
@ -107,6 +120,12 @@ public class SettingsTabDtr : SettingsTab
// }
}
if (moveMouseToIndex >= 0 && moveMouseToIndex < moveMouseTo.Length)
{
ImGui.GetIO().WantSetMousePos = true;
ImGui.GetIO().MousePos = moveMouseTo[moveMouseToIndex];
}
configuration.DtrOrder = order.Concat(orderLeft).ToList();
configuration.DtrIgnore = ignore.Concat(ignoreLeft).ToList();