Cleanup TextureLoadThrottler

This commit is contained in:
Soreepeong 2024-03-01 00:10:58 +09:00
parent b34a901702
commit e2ed5258eb
5 changed files with 116 additions and 119 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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;
}
}
}
}

View file

@ -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);