mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-27 10:59:18 +01:00
Cleanup TextureLoadThrottler
This commit is contained in:
parent
b34a901702
commit
e2ed5258eb
5 changed files with 116 additions and 119 deletions
|
|
@ -36,7 +36,7 @@ internal sealed class FileSystemSharedImmediateTexture : SharedImmediateTexture
|
|||
|
||||
/// <inheritdoc/>
|
||||
protected override void ReviveResources() =>
|
||||
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
|
||||
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().LoadTextureAsync(
|
||||
this,
|
||||
this.CreateTextureAsync,
|
||||
this.LoadCancellationToken);
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
|
|||
|
||||
/// <inheritdoc/>
|
||||
protected override void ReviveResources() =>
|
||||
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
|
||||
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().LoadTextureAsync(
|
||||
this,
|
||||
this.CreateTextureAsync,
|
||||
this.LoadCancellationToken);
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe
|
|||
|
||||
/// <inheritdoc/>
|
||||
protected override void ReviveResources() =>
|
||||
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader(
|
||||
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().LoadTextureAsync(
|
||||
this,
|
||||
this.CreateTextureAsync,
|
||||
this.LoadCancellationToken);
|
||||
|
|
|
|||
|
|
@ -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<WorkItem> newItemChannel = Channel.CreateUnbounded<WorkItem>();
|
||||
private readonly Channel<object?> workTokenChannel = Channel.CreateUnbounded<object?>();
|
||||
private readonly List<WorkItem> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Basis for throttling.
|
||||
/// </summary>
|
||||
/// <summary>Basis for throttling. Values may be changed anytime.</summary>
|
||||
internal interface IThrottleBasisProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the resource is requested in an opportunistic way.
|
||||
/// </summary>
|
||||
/// <summary>Gets a value indicating whether the resource is requested in an opportunistic way.</summary>
|
||||
bool IsOpportunistic { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first requested tick count from <see cref="Environment.TickCount64"/>.
|
||||
/// </summary>
|
||||
/// <summary>Gets the first requested tick count from <see cref="Environment.TickCount64"/>.</summary>
|
||||
long FirstRequestedTick { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest requested tick count from <see cref="Environment.TickCount64"/>.
|
||||
/// </summary>
|
||||
/// <summary>Gets the latest requested tick count from <see cref="Environment.TickCount64"/>.</summary>
|
||||
long LatestRequestedTick { get; }
|
||||
}
|
||||
|
||||
|
|
@ -72,135 +63,94 @@ internal class TextureLoadThrottler : IServiceType, IDisposable
|
|||
_ = t.Exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a texture loader.
|
||||
/// </summary>
|
||||
/// <summary>Loads a texture according to some order.</summary>
|
||||
/// <param name="basis">The throttle basis.</param>
|
||||
/// <param name="immediateLoadFunction">The immediate load function.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The task.</returns>
|
||||
public Task<IDalamudTextureWrap> CreateLoader(
|
||||
public Task<IDalamudTextureWrap> LoadTextureAsync(
|
||||
IThrottleBasisProvider basis,
|
||||
Func<CancellationToken, Task<IDalamudTextureWrap>> 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<IDalamudTextureWrap>(new ObjectDisposedException(nameof(TextureLoadThrottler)));
|
||||
}
|
||||
|
||||
private async Task LoopAddWorkItemAsync()
|
||||
{
|
||||
var newWorkTemp = new List<WorkItem>();
|
||||
const int batchAddSize = 64;
|
||||
var newWorks = new List<WorkItem>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Extracts the work item with the highest priority from <see cref="workItemPending"/>,
|
||||
/// and removes cancelled items, if any.</summary>
|
||||
/// <remarks>The order of items of <see cref="workItemPending"/> is undefined after this function.</remarks>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an item in <see cref="workItemPending"/>, avoiding shifting.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of the item to remove.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A read-only implementation of <see cref="IThrottleBasisProvider"/>.
|
||||
/// </summary>
|
||||
|
|
@ -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<WorkItem>
|
||||
private class WorkItem : IComparable<WorkItem>
|
||||
{
|
||||
public required TaskCompletionSource<IDalamudTextureWrap> TaskCompletionSource { get; init; }
|
||||
private readonly TaskCompletionSource<IDalamudTextureWrap> taskCompletionSource;
|
||||
private readonly IThrottleBasisProvider basis;
|
||||
private readonly CancellationToken cancellationToken;
|
||||
private readonly Func<CancellationToken, Task<IDalamudTextureWrap>> immediateLoadFunction;
|
||||
|
||||
public required IThrottleBasisProvider Basis { get; init; }
|
||||
public WorkItem(
|
||||
IThrottleBasisProvider basis,
|
||||
Func<CancellationToken, Task<IDalamudTextureWrap>> immediateLoadFunction,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
this.taskCompletionSource = new();
|
||||
this.basis = basis;
|
||||
this.cancellationToken = cancellationToken;
|
||||
this.immediateLoadFunction = immediateLoadFunction;
|
||||
}
|
||||
|
||||
public required CancellationToken CancellationToken { get; init; }
|
||||
|
||||
public required Func<CancellationToken, Task<IDalamudTextureWrap>> ImmediateLoadFunction { get; init; }
|
||||
public Task<IDalamudTextureWrap> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
|
|||
public Task<IDalamudTextureWrap> CreateFromImageAsync(
|
||||
ReadOnlyMemory<byte> 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<byte> 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<IDalamudTextureWrap> CreateFromTexFileAsync(
|
||||
TexFile file,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
this.textureLoadThrottler.CreateLoader(
|
||||
this.textureLoadThrottler.LoadTextureAsync(
|
||||
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
|
||||
ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct),
|
||||
cancellationToken);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue