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/> /// <inheritdoc/>
protected override void ReviveResources() => protected override void ReviveResources() =>
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader( this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().LoadTextureAsync(
this, this,
this.CreateTextureAsync, this.CreateTextureAsync,
this.LoadCancellationToken); this.LoadCancellationToken);

View file

@ -38,7 +38,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
/// <inheritdoc/> /// <inheritdoc/>
protected override void ReviveResources() => protected override void ReviveResources() =>
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader( this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().LoadTextureAsync(
this, this,
this.CreateTextureAsync, this.CreateTextureAsync,
this.LoadCancellationToken); this.LoadCancellationToken);

View file

@ -45,7 +45,7 @@ internal sealed class ManifestResourceSharedImmediateTexture : SharedImmediateTe
/// <inheritdoc/> /// <inheritdoc/>
protected override void ReviveResources() => protected override void ReviveResources() =>
this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().CreateLoader( this.UnderlyingWrap = Service<TextureLoadThrottler>.Get().LoadTextureAsync(
this, this,
this.CreateTextureAsync, this.CreateTextureAsync,
this.LoadCancellationToken); this.LoadCancellationToken);

View file

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Channels; using System.Threading.Channels;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,7 +16,6 @@ internal class TextureLoadThrottler : IServiceType, IDisposable
private readonly Task adderTask; private readonly Task adderTask;
private readonly Task[] workerTasks; private readonly Task[] workerTasks;
private readonly object workListLock = new();
private readonly Channel<WorkItem> newItemChannel = Channel.CreateUnbounded<WorkItem>(); private readonly Channel<WorkItem> newItemChannel = Channel.CreateUnbounded<WorkItem>();
private readonly Channel<object?> workTokenChannel = Channel.CreateUnbounded<object?>(); private readonly Channel<object?> workTokenChannel = Channel.CreateUnbounded<object?>();
private readonly List<WorkItem> workItemPending = new(); private readonly List<WorkItem> workItemPending = new();
@ -27,29 +26,21 @@ internal class TextureLoadThrottler : IServiceType, IDisposable
private TextureLoadThrottler() private TextureLoadThrottler()
{ {
this.adderTask = Task.Run(this.LoopAddWorkItemAsync); 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()) foreach (ref var task in this.workerTasks.AsSpan())
task = Task.Run(this.LoopProcessWorkItemAsync); task = Task.Run(this.LoopProcessWorkItemAsync);
} }
/// <summary> /// <summary>Basis for throttling. Values may be changed anytime.</summary>
/// Basis for throttling.
/// </summary>
internal interface IThrottleBasisProvider internal interface IThrottleBasisProvider
{ {
/// <summary> /// <summary>Gets a value indicating whether the resource is requested in an opportunistic way.</summary>
/// Gets a value indicating whether the resource is requested in an opportunistic way.
/// </summary>
bool IsOpportunistic { get; } bool IsOpportunistic { get; }
/// <summary> /// <summary>Gets the first requested tick count from <see cref="Environment.TickCount64"/>.</summary>
/// Gets the first requested tick count from <see cref="Environment.TickCount64"/>.
/// </summary>
long FirstRequestedTick { get; } long FirstRequestedTick { get; }
/// <summary> /// <summary>Gets the latest requested tick count from <see cref="Environment.TickCount64"/>.</summary>
/// Gets the latest requested tick count from <see cref="Environment.TickCount64"/>.
/// </summary>
long LatestRequestedTick { get; } long LatestRequestedTick { get; }
} }
@ -72,135 +63,94 @@ internal class TextureLoadThrottler : IServiceType, IDisposable
_ = t.Exception; _ = t.Exception;
} }
/// <summary> /// <summary>Loads a texture according to some order.</summary>
/// Creates a texture loader.
/// </summary>
/// <param name="basis">The throttle basis.</param> /// <param name="basis">The throttle basis.</param>
/// <param name="immediateLoadFunction">The immediate load function.</param> /// <param name="immediateLoadFunction">The immediate load function.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task.</returns> /// <returns>The task.</returns>
public Task<IDalamudTextureWrap> CreateLoader( public Task<IDalamudTextureWrap> LoadTextureAsync(
IThrottleBasisProvider basis, IThrottleBasisProvider basis,
Func<CancellationToken, Task<IDalamudTextureWrap>> immediateLoadFunction, Func<CancellationToken, Task<IDalamudTextureWrap>> immediateLoadFunction,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var work = new WorkItem var work = new WorkItem(basis, immediateLoadFunction, cancellationToken);
{
TaskCompletionSource = new(),
Basis = basis,
CancellationToken = cancellationToken,
ImmediateLoadFunction = immediateLoadFunction,
};
return return
this.newItemChannel.Writer.TryWrite(work) this.newItemChannel.Writer.TryWrite(work)
? work.TaskCompletionSource.Task ? work.Task
: Task.FromException<IDalamudTextureWrap>(new ObjectDisposedException(nameof(TextureLoadThrottler))); : Task.FromException<IDalamudTextureWrap>(new ObjectDisposedException(nameof(TextureLoadThrottler)));
} }
private async Task LoopAddWorkItemAsync() private async Task LoopAddWorkItemAsync()
{ {
var newWorkTemp = new List<WorkItem>(); const int batchAddSize = 64;
var newWorks = new List<WorkItem>(batchAddSize);
var reader = this.newItemChannel.Reader; 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); lock (this.workItemPending)
while (newWorkTemp.Count < newWorkTemp.Capacity && reader.TryRead(out var newWork)) this.workItemPending.AddRange(newWorks);
newWorkTemp.Add(newWork);
lock (this.workListLock) for (var i = newWorks.Count; i > 0; i--)
this.workItemPending.AddRange(newWorkTemp);
for (var i = newWorkTemp.Count; i > 0; i--)
this.workTokenChannel.Writer.TryWrite(null); this.workTokenChannel.Writer.TryWrite(null);
newWorkTemp.Clear();
newWorks.Clear();
} }
} }
private async Task LoopProcessWorkItemAsync() private async Task LoopProcessWorkItemAsync()
{ {
var reader = this.workTokenChannel.Reader; 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) if (this.ExtractHighestPriorityWorkItem() is not { } work)
continue; continue;
try await work.Process(this.disposeCancellationTokenSource.Token);
{
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;
}
} }
} }
/// <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() private WorkItem? ExtractHighestPriorityWorkItem()
{ {
lock (this.workListLock) lock (this.workItemPending)
{ {
WorkItem? highestPriorityWork = null; for (var startIndex = 0; startIndex < this.workItemPending.Count - 1;)
var highestPriorityIndex = -1;
for (var i = 0; i < this.workItemPending.Count; i++)
{ {
var work = this.workItemPending[i]; var span = CollectionsMarshal.AsSpan(this.workItemPending)[startIndex..];
if (work.CancellationToken.IsCancellationRequested) ref var lastRef = ref span[^1];
foreach (ref var itemRef in span[..^1])
{ {
work.TaskCompletionSource.SetCanceled(work.CancellationToken); if (itemRef.CancelAsRequested())
_ = work.TaskCompletionSource.Task.Exception; {
this.RelocatePendingWorkItemToEndAndEraseUnsafe(i--); itemRef = lastRef;
continue; this.workItemPending.RemoveAt(this.workItemPending.Count - 1);
} break;
}
if (highestPriorityIndex == -1 || if (itemRef.CompareTo(lastRef) < 0)
work.CompareTo(this.workItemPending[highestPriorityIndex]) < 0) (itemRef, lastRef) = (lastRef, itemRef);
{ startIndex++;
highestPriorityIndex = i;
highestPriorityWork = work;
} }
} }
if (highestPriorityWork is null) if (this.workItemPending.Count == 0)
return null; return null;
this.RelocatePendingWorkItemToEndAndEraseUnsafe(highestPriorityIndex); var last = this.workItemPending[^1];
return highestPriorityWork; 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> /// <summary>
/// A read-only implementation of <see cref="IThrottleBasisProvider"/>. /// A read-only implementation of <see cref="IThrottleBasisProvider"/>.
/// </summary> /// </summary>
@ -216,27 +166,74 @@ internal class TextureLoadThrottler : IServiceType, IDisposable
public long LatestRequestedTick { get; init; } = Environment.TickCount64; public long LatestRequestedTick { get; init; } = Environment.TickCount64;
} }
[SuppressMessage( private class WorkItem : IComparable<WorkItem>
"StyleCop.CSharp.OrderingRules",
"SA1206:Declaration keywords should follow order",
Justification = "no")]
private record 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 Task<IDalamudTextureWrap> Task => this.taskCompletionSource.Task;
public required Func<CancellationToken, Task<IDalamudTextureWrap>> ImmediateLoadFunction { get; init; }
public int CompareTo(WorkItem other) public int CompareTo(WorkItem other)
{ {
if (this.Basis.IsOpportunistic != other.Basis.IsOpportunistic) if (this.basis.IsOpportunistic != other.basis.IsOpportunistic)
return this.Basis.IsOpportunistic ? 1 : -1; return this.basis.IsOpportunistic ? 1 : -1;
if (this.Basis.IsOpportunistic) if (this.basis.IsOpportunistic)
return -this.Basis.LatestRequestedTick.CompareTo(other.Basis.LatestRequestedTick); return -this.basis.LatestRequestedTick.CompareTo(other.basis.LatestRequestedTick);
return this.Basis.FirstRequestedTick.CompareTo(other.Basis.FirstRequestedTick); 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( public Task<IDalamudTextureWrap> CreateFromImageAsync(
ReadOnlyMemory<byte> bytes, ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader( this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray()), ct), ct => Task.Run(() => this.NoThrottleCreateFromImage(bytes.ToArray()), ct),
cancellationToken); cancellationToken);
@ -229,7 +229,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
Stream stream, Stream stream,
bool leaveOpen = false, bool leaveOpen = false,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader( this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct => async ct =>
{ {
@ -300,7 +300,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
RawImageSpecification specs, RawImageSpecification specs,
ReadOnlyMemory<byte> bytes, ReadOnlyMemory<byte> bytes,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader( this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
_ => Task.FromResult(this.CreateFromRaw(specs, bytes.Span)), _ => Task.FromResult(this.CreateFromRaw(specs, bytes.Span)),
cancellationToken); cancellationToken);
@ -311,7 +311,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
Stream stream, Stream stream,
bool leaveOpen = false, bool leaveOpen = false,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader( this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
async ct => async ct =>
{ {
@ -337,7 +337,7 @@ internal sealed class TextureManager : IServiceType, IDisposable, ITextureProvid
public Task<IDalamudTextureWrap> CreateFromTexFileAsync( public Task<IDalamudTextureWrap> CreateFromTexFileAsync(
TexFile file, TexFile file,
CancellationToken cancellationToken = default) => CancellationToken cancellationToken = default) =>
this.textureLoadThrottler.CreateLoader( this.textureLoadThrottler.LoadTextureAsync(
new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(), new TextureLoadThrottler.ReadOnlyThrottleBasisProvider(),
ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct), ct => Task.Run(() => this.NoThrottleCreateFromTexFile(file), ct),
cancellationToken); cancellationToken);