Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
Expand All @@ -13,6 +13,7 @@
using AsyncAwaitBestPractices;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ExifLibrary;
using FluentAvalonia.UI.Controls;
Expand Down Expand Up @@ -75,6 +76,115 @@ public abstract partial class InferenceGenerationViewModelBase
[JsonIgnore]
public IInferenceClientManager ClientManager { get; }

private readonly List<InferenceProjectDocument> _generationQueue = [];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _generationQueue list is accessed and modified from both the UI thread (via ClearQueue and QueueGeneration commands) and potentially a background thread (via the ProcessQueueAsync loop). While Avalonia's DispatcherSynchronizationContext often keeps continuations on the UI thread, relying on it for thread safety of a standard List<T> across multiple await points is risky. Consider using a thread-safe collection like ConcurrentQueue<InferenceProjectDocument> or adding a lock around all accesses to _generationQueue.

private bool _isProcessingQueue;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(QueueToggleIcon))]
[NotifyPropertyChangedFor(nameof(QueueToggleToolTip))]
[property: JsonIgnore]
private bool isQueuePaused = true;

public string QueueToggleIcon => IsQueuePaused ? "fa-solid fa-play" : "fa-solid fa-pause";
public string QueueToggleToolTip => IsQueuePaused ? "Start Queue" : "Pause Queue";

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(QueueGenerationText))]
[NotifyPropertyChangedFor(nameof(IsQueueClearable))]
[property: JsonIgnore]
private int queuedGenerationsCount;

public string QueueGenerationText =>
QueuedGenerationsCount > 0 ? $"Queue Generation ({QueuedGenerationsCount})" : "Queue Generation";

public bool IsQueueClearable => QueuedGenerationsCount > 0;

[RelayCommand]
private void ClearQueue()
{
_generationQueue.Clear();
QueuedGenerationsCount = 0;
IsQueuePaused = true;
Logger.Info("Generation queue cleared");
}

[RelayCommand]
private void ToggleQueueState()
{
IsQueuePaused = !IsQueuePaused;
if (!IsQueuePaused)
{
ProcessQueueAsync().SafeFireAndForget(ex => Logger.Error(ex, "Error processing generation queue"));
}
}

[RelayCommand]
private void QueueGeneration()
{
var doc = InferenceProjectDocument.FromLoadable(this);
_generationQueue.Add(doc);
QueuedGenerationsCount = _generationQueue.Count;
Logger.Info("Queued generation. Queue size: {QueueSize}", QueuedGenerationsCount);

if (!IsQueuePaused)
{
ProcessQueueAsync().SafeFireAndForget(ex => Logger.Error(ex, "Error processing generation queue"));
}
}

private async Task ProcessQueueAsync()
{
if (_isProcessingQueue)
return;

_isProcessingQueue = true;

try
{
while (_generationQueue.Count > 0 && !IsQueuePaused)
{
// Wait for any active generation to complete before starting the next
if (GenerateImageCommand.IsRunning)
{
var executionTask = GenerateImageCommand.ExecutionTask;
if (executionTask is not null)
{
await executionTask;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Awaiting executionTask directly here can cause the entire queue processing loop to terminate if the task faults (e.g., due to an unhandled exception in a manual generation run). Since ProcessQueueAsync is intended to be a resilient background processor, it should handle potential failures of the tasks it waits on to ensure the rest of the queue can still be processed.

                try
                {
                    await executionTask;
                }
                catch (Exception ex)
                {
                    Logger.Warn(ex, "Previous generation task failed, continuing queue");
                }

}
}

// Re-check after awaiting — queue may have been cleared or paused
if (_generationQueue.Count == 0 || IsQueuePaused)
break;

// Dequeue and load state on UI thread
var nextDoc = _generationQueue[0];
_generationQueue.RemoveAt(0);
QueuedGenerationsCount = _generationQueue.Count;

await Dispatcher.UIThread.InvokeAsync(() => LoadStateFromJsonObject(nextDoc.State));

try
{
await GenerateImageCommand.ExecuteAsync(default(GenerateFlags));
}
catch (Exception ex)
{
Logger.Error(ex, "Queued generation failed");
}
}

if (_generationQueue.Count == 0)
{
IsQueuePaused = true;
}
}
finally
{
_isProcessingQueue = false;
}
}

/// <inheritdoc />
protected InferenceGenerationViewModelBase(
IServiceManager<ViewModelBase> vmFactory,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<dock:DockUserControlBase
<dock:DockUserControlBase
x:Class="StabilityMatrix.Avalonia.Views.Inference.InferenceTextToImageView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Expand Down Expand Up @@ -105,7 +105,7 @@
DataContext="{Binding ElementName=Dock, Path=DataContext}">
<Grid
HorizontalAlignment="Center"
ColumnDefinitions="Auto,*,Auto"
ColumnDefinitions="Auto,Auto,Auto,Auto"
RowDefinitions="Auto,*">
<Grid.Styles>
<Style Selector="Button">
Expand Down Expand Up @@ -152,9 +152,33 @@
Command="{Binding GenerateImageCancelCommand}"
IsVisible="{Binding GenerateImageCommand.CanBeCanceled}" />

<!-- Queue Generation Button -->
<StackPanel Grid.Row="1" Grid.Column="2" Orientation="Horizontal" Margin="4,0,0,0">
<Button
MinWidth="130"
MaxWidth="200"
HorizontalAlignment="Stretch"
Command="{Binding QueueGenerationCommand}"
Content="{Binding QueueGenerationText}" />
<Button
Margin="4,0,0,0"
Padding="12,8"
icons:Attached.Icon="{Binding QueueToggleIcon}"
ToolTip.Tip="{Binding QueueToggleToolTip}"
IsVisible="{Binding IsQueueClearable}"
Command="{Binding ToggleQueueStateCommand}" />
<Button
Margin="4,0,0,0"
Padding="12,8"
icons:Attached.Icon="fa-solid fa-trash"
ToolTip.Tip="Clear Queue"
IsVisible="{Binding IsQueueClearable}"
Command="{Binding ClearQueueCommand}" />
</StackPanel>

<Button
Grid.Row="1"
Grid.Column="2"
Grid.Column="3"
Margin="4,0"
Padding="12,8"
HorizontalAlignment="Left"
Expand Down
Loading