diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 734f2d0f4..702a83f52 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -36,7 +36,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture /// protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().CreateLoader( + this.UnderlyingWrap = Service.Get().LoadTextureAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 8b97d04d2..e22998813 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -38,7 +38,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture /// protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().CreateLoader( + this.UnderlyingWrap = Service.Get().LoadTextureAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); diff --git a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index a249be80e..a75a7cb68 100644 --- a/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -45,7 +45,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe /// protected override void ReviveResources() => - this.UnderlyingWrap = Service.Get().CreateLoader( + this.UnderlyingWrap = Service.Get().LoadTextureAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); diff --git a/Dalamud/Interface/Internal/TextureLoadThrottler.cs b/Dalamud/Interface/Internal/TextureLoadThrottler.cs index 894e5308e..978d7b9b7 100644 --- a/Dalamud/Interface/Internal/TextureLoadThrottler.cs +++ b/Dalamud/Interface/Internal/TextureLoadThrottler.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; @@ -16,7 +16,6 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private readonly Task adderTask; private readonly Task[] workerTasks; - private readonly object workListLock = new(); private readonly Channel newItemChannel = Channel.CreateUnbounded(); private readonly Channel workTokenChannel = Channel.CreateUnbounded(); private readonly List workItemPending = new(); @@ -27,29 +26,21 @@ internal class TextureLoadThrottler : IServiceType, IDisposable private TextureLoadThrottler() { this.adderTask = Task.Run(this.LoopAddWorkItemAsync); - this.workerTasks = new Task[Math.Min(64, Environment.ProcessorCount)]; + this.workerTasks = new Task[Math.Max(1, Environment.ProcessorCount - 1)]; foreach (ref var task in this.workerTasks.AsSpan()) task = Task.Run(this.LoopProcessWorkItemAsync); } - /// - /// Basis for throttling. - /// + /// Basis for throttling. Values may be changed anytime. internal interface IThrottleBasisProvider { - /// - /// Gets a value indicating whether the resource is requested in an opportunistic way. - /// + /// Gets a value indicating whether the resource is requested in an opportunistic way. bool IsOpportunistic { get; } - /// - /// Gets the first requested tick count from . - /// + /// Gets the first requested tick count from . long FirstRequestedTick { get; } - /// - /// Gets the latest requested tick count from . - /// + /// Gets the latest requested tick count from . long LatestRequestedTick { get; } } @@ -72,135 +63,94 @@ internal class TextureLoadThrottler : IServiceType, IDisposable _ = t.Exception; } - /// - /// Creates a texture loader. - /// + /// Loads a texture according to some order. /// The throttle basis. /// The immediate load function. /// The cancellation token. /// The task. - public Task CreateLoader( + public Task LoadTextureAsync( IThrottleBasisProvider basis, Func> immediateLoadFunction, CancellationToken cancellationToken) { - var work = new WorkItem - { - TaskCompletionSource = new(), - Basis = basis, - CancellationToken = cancellationToken, - ImmediateLoadFunction = immediateLoadFunction, - }; + var work = new WorkItem(basis, immediateLoadFunction, cancellationToken); return this.newItemChannel.Writer.TryWrite(work) - ? work.TaskCompletionSource.Task + ? work.Task : Task.FromException(new ObjectDisposedException(nameof(TextureLoadThrottler))); } private async Task LoopAddWorkItemAsync() { - var newWorkTemp = new List(); + const int batchAddSize = 64; + var newWorks = new List(batchAddSize); var reader = this.newItemChannel.Reader; - while (!reader.Completion.IsCompleted) + while (await reader.WaitToReadAsync()) { - await reader.WaitToReadAsync(); + while (newWorks.Count < batchAddSize && reader.TryRead(out var newWork)) + newWorks.Add(newWork); - newWorkTemp.EnsureCapacity(reader.Count); - while (newWorkTemp.Count < newWorkTemp.Capacity && reader.TryRead(out var newWork)) - newWorkTemp.Add(newWork); - lock (this.workListLock) - this.workItemPending.AddRange(newWorkTemp); - for (var i = newWorkTemp.Count; i > 0; i--) + lock (this.workItemPending) + this.workItemPending.AddRange(newWorks); + + for (var i = newWorks.Count; i > 0; i--) this.workTokenChannel.Writer.TryWrite(null); - newWorkTemp.Clear(); + + newWorks.Clear(); } } private async Task LoopProcessWorkItemAsync() { var reader = this.workTokenChannel.Reader; - while (!reader.Completion.IsCompleted) + while (await reader.WaitToReadAsync()) { - _ = await reader.ReadAsync(); + if (!reader.TryRead(out _)) + continue; if (this.ExtractHighestPriorityWorkItem() is not { } work) continue; - try - { - IDalamudTextureWrap wrap; - if (work.CancellationToken.CanBeCanceled) - { - using var cts = CancellationTokenSource.CreateLinkedTokenSource( - this.disposeCancellationTokenSource.Token, - work.CancellationToken); - wrap = await work.ImmediateLoadFunction(cts.Token); - } - else - { - wrap = await work.ImmediateLoadFunction(this.disposeCancellationTokenSource.Token); - } - - work.TaskCompletionSource.SetResult(wrap); - } - catch (Exception e) - { - work.TaskCompletionSource.SetException(e); - _ = work.TaskCompletionSource.Task.Exception; - } + await work.Process(this.disposeCancellationTokenSource.Token); } } + /// Extracts the work item with the highest priority from , + /// and removes cancelled items, if any. + /// The order of items of is undefined after this function. private WorkItem? ExtractHighestPriorityWorkItem() { - lock (this.workListLock) + lock (this.workItemPending) { - WorkItem? highestPriorityWork = null; - var highestPriorityIndex = -1; - for (var i = 0; i < this.workItemPending.Count; i++) + for (var startIndex = 0; startIndex < this.workItemPending.Count - 1;) { - var work = this.workItemPending[i]; - if (work.CancellationToken.IsCancellationRequested) + var span = CollectionsMarshal.AsSpan(this.workItemPending)[startIndex..]; + ref var lastRef = ref span[^1]; + foreach (ref var itemRef in span[..^1]) { - work.TaskCompletionSource.SetCanceled(work.CancellationToken); - _ = work.TaskCompletionSource.Task.Exception; - this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); - continue; - } + if (itemRef.CancelAsRequested()) + { + itemRef = lastRef; + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); + break; + } - if (highestPriorityIndex == -1 || - work.CompareTo(this.workItemPending[highestPriorityIndex]) < 0) - { - highestPriorityIndex = i; - highestPriorityWork = work; + if (itemRef.CompareTo(lastRef) < 0) + (itemRef, lastRef) = (lastRef, itemRef); + startIndex++; } } - if (highestPriorityWork is null) + if (this.workItemPending.Count == 0) return null; - this.RelocatePendingWorkItemToEndAndEraseUnsafe(highestPriorityIndex); - return highestPriorityWork; + var last = this.workItemPending[^1]; + this.workItemPending.RemoveAt(this.workItemPending.Count - 1); + return last.CancelAsRequested() ? null : last; } } - /// - /// Remove an item in , avoiding shifting. - /// - /// Index of the item to remove. - private void RelocatePendingWorkItemToEndAndEraseUnsafe(int index) - { - // Relocate the element to remove to the last. - if (index != this.workItemPending.Count - 1) - { - (this.workItemPending[^1], this.workItemPending[index]) = - (this.workItemPending[index], this.workItemPending[^1]); - } - - this.workItemPending.RemoveAt(this.workItemPending.Count - 1); - } - /// /// A read-only implementation of . /// @@ -216,27 +166,74 @@ internal class TextureLoadThrottler : IServiceType, IDisposable public long LatestRequestedTick { get; init; } = Environment.TickCount64; } - [SuppressMessage( - "StyleCop.CSharp.OrderingRules", - "SA1206:Declaration keywords should follow order", - Justification = "no")] - private record WorkItem : IComparable + private class WorkItem : IComparable { - public required TaskCompletionSource TaskCompletionSource { get; init; } + private readonly TaskCompletionSource taskCompletionSource; + private readonly IThrottleBasisProvider basis; + private readonly CancellationToken cancellationToken; + private readonly Func> immediateLoadFunction; - public required IThrottleBasisProvider Basis { get; init; } + public WorkItem( + IThrottleBasisProvider basis, + Func> immediateLoadFunction, + CancellationToken cancellationToken) + { + this.taskCompletionSource = new(); + this.basis = basis; + this.cancellationToken = cancellationToken; + this.immediateLoadFunction = immediateLoadFunction; + } - public required CancellationToken CancellationToken { get; init; } - - public required Func> ImmediateLoadFunction { get; init; } + public Task Task => this.taskCompletionSource.Task; public int CompareTo(WorkItem other) { - if (this.Basis.IsOpportunistic != other.Basis.IsOpportunistic) - return this.Basis.IsOpportunistic ? 1 : -1; - if (this.Basis.IsOpportunistic) - return -this.Basis.LatestRequestedTick.CompareTo(other.Basis.LatestRequestedTick); - return this.Basis.FirstRequestedTick.CompareTo(other.Basis.FirstRequestedTick); + if (this.basis.IsOpportunistic != other.basis.IsOpportunistic) + return this.basis.IsOpportunistic ? 1 : -1; + if (this.basis.IsOpportunistic) + return -this.basis.LatestRequestedTick.CompareTo(other.basis.LatestRequestedTick); + return this.basis.FirstRequestedTick.CompareTo(other.basis.FirstRequestedTick); + } + + public bool CancelAsRequested() + { + if (!this.cancellationToken.IsCancellationRequested) + return false; + + // Cancel the load task and move on. + this.taskCompletionSource.TrySetCanceled(this.cancellationToken); + + // Suppress the OperationCanceledException caused from the above. + _ = this.taskCompletionSource.Task.Exception; + + return true; + } + + public async ValueTask Process(CancellationToken serviceDisposeToken) + { + try + { + IDalamudTextureWrap wrap; + if (this.cancellationToken.CanBeCanceled) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + serviceDisposeToken, + this.cancellationToken); + wrap = await this.immediateLoadFunction(cts.Token); + } + else + { + wrap = await this.immediateLoadFunction(serviceDisposeToken); + } + + if (!this.taskCompletionSource.TrySetResult(wrap)) + wrap.Dispose(); + } + catch (Exception e) + { + this.taskCompletionSource.TrySetException(e); + _ = this.taskCompletionSource.Task.Exception; + } } } } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 6ad768e76..0d0d3a835 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -219,7 +219,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid public Task CreateFromImageAsync( ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray()), ct), cancellationToken); @@ -229,7 +229,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), async ct => { @@ -300,7 +300,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid RawImageSpecification specs, ReadOnlyMemory bytes, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), _ => Task.FromResult(this.CreateFromRaw(specs, bytes.Span)), cancellationToken); @@ -311,7 +311,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid Stream stream, bool leaveOpen = false, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), async ct => { @@ -337,7 +337,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid public Task CreateFromTexFileAsync( TexFile file, CancellationToken cancellationToken = default) => - this.textureLoadThrottler.CreateLoader( + this.textureLoadThrottler.LoadTextureAsync( new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), cancellationToken);