Skip to content
Merged
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
Expand Up @@ -60,20 +60,61 @@ protected override async Task OnInitializedAsync()

private bool IsDirectoryAvailable => this.directoryInfo.Exists;

/// <summary>
/// Takes the next file which the directory scan found.
/// </summary>
/// <remarks>
/// This runs on the scan's own thread, and it does so deliberately, although the scan asks its
/// callers to reach for a dispatcher. That request is about updating the UI, and none of these
/// callbacks does: they write fields and nothing else.<br/><br/>
/// Why that holds is worth writing down, because the code does not show it. The string builder
/// has exactly one writer -- this method, on that one thread -- and nobody else ever reads it.
/// What the renderer reads is the text field beside it, and assigning a string reference is
/// atomic, so a render sees the whole previous text or the whole new one, never half of either.
/// Building that text anew costs little, because the scan stops reporting files once it has
/// reported a hundred. And a render happens only when the refresh timer ticks, which goes
/// through the dispatcher, so a reading taken a moment too early is replaced 1.6 seconds later
/// anyway.
/// </remarks>
private void UpdateFileList(string file)
{
this.directoryFiles.Append("- ");
this.directoryFiles.AppendLine(file);
this.directoryFilesText = this.directoryFiles.ToString();
}

/// <summary>
/// Takes the size which the directory scan has added up so far.
/// </summary>
/// <remarks>
/// Two threads call this, but never at the same time: the scan reports its progress from its
/// own thread, and the final figure follows once that thread has finished. A long is written in
/// one piece on all six targets we ship, which are 64 bit throughout, and the renderer reads it
/// only when the refresh timer ticks. The remark on the file list carries the reasoning these
/// callbacks share.
/// </remarks>
private void UpdateDirectorySize(long size)
{
this.directorySizeBytes = size;
}

/// <summary>
/// Takes the number of files which the directory scan has counted so far.
/// </summary>
/// <remarks>
/// Reported from the same two threads as the size above, and safe for the same reason.
/// </remarks>
private void UpdateDirectoryFiles(long numFiles) => this.directorySizeNumFiles = numFiles;

/// <summary>
/// Takes the news that the directory scan has finished.
/// </summary>
/// <remarks>
/// This one, unlike the three above, does not run on the scan's thread. The scan invokes it
/// after awaiting its worker, and that continuation returns to the dispatcher this dialog was
/// initialized on. Stopping the timer and asking for a render from here is therefore no
/// different from doing either in a lifecycle method.
/// </remarks>
private void DirectoryOperationDone()
{
this.refreshTimer.Stop();
Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Pages/Chat.razor
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
</CascadingValue>
@if (this.AreWorkspacesVisible)
{
<MudSplitter Dimension="@this.ReadSplitterPosition" DimensionChanged="this.SplitterChanged" EnableSlide="@this.AreWorkspacesVisible" EnableMargin="@false" StartContentStyle="margin-right: 1em;" BarStyle="" EndContentStyle="margin-left: 1em;">
<MudSplitter Dimension="@this.ReadSplitterPosition" DimensionChanged="this.SplitterChanged" Sensitivity="0.05" EnableSlide="@this.AreWorkspacesVisible" EnableMargin="@false" StartContentStyle="margin-right: 1em;" BarStyle="" EndContentStyle="margin-left: 1em;">
<StartContent>
@if (this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_SIDEBAR && this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible)
{
Expand Down
38 changes: 37 additions & 1 deletion app/MindWork AI Studio/Pages/Chat.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public partial class Chat : MSGComponentBase
private string currentWorkspaceName = string.Empty;
private Workspaces? workspaces;
private double splitterPosition = 30;
private bool skipRenderAfterSplitterChange;
private readonly ChatComposerState composerState = new();

private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6));
Expand All @@ -39,6 +40,16 @@ protected override async Task OnInitializedAsync()

this.splitterPosition = this.SettingsManager.ConfigurationData.Workspace.SplitterPosition;
this.splitterSaveTimer.AutoReset = false;
//
// Mind that this handler deliberately stays off the renderer thread, although it writes the
// configuration data from a thread pool thread. The position is a single double, and every
// target we ship is 64 bit, so the write cannot tear -- and the worst a lost one could do is
// a splitter standing somewhere else after the next start. What a jump to the dispatcher
// would cost instead is paid by the user: storing the settings serializes all of them and
// writes two files, and it would do that in the very queue which draws the drag they are in
// the middle of. The splitter then stutters under their hand. Whoever synchronizes the
// configuration data one day should do it without moving that work onto the renderer.
//
this.splitterSaveTimer.Elapsed += (_, _) =>
{
this.SettingsManager.ConfigurationData.Workspace.SplitterPosition = this.splitterPosition;
Expand All @@ -47,7 +58,31 @@ protected override async Task OnInitializedAsync()

await base.OnInitializedAsync();
}


/// <summary>
/// Decides whether this page renders again.
/// </summary>
/// <remarks>
/// Dragging the splitter reports every movement, and Blazor renders this page after each of
/// them. That render is pure waste: all it would contribute is the position the splitter just
/// reported, and the splitter has it already -- it renders itself after its own event, which is
/// what resizes the two panels. What this page rebuilds instead is everything else it holds,
/// the workspace tree above all, which has no render guard of its own and draws an item with
/// three buttons for every chat. That is what the user sees stutter while they drag.<br/><br/>
/// Dropping that one render costs nothing, because the splitter never needed it. Should a
/// message from the bus ask for a render in the very same moment, this swallows it -- both sit
/// on the same dispatcher and the render of a movement follows it without a gap, so the window
/// is as good as closed, and the next render brings the message along anyway.
/// </remarks>
protected override bool ShouldRender()
{
if (!this.skipRenderAfterSplitterChange)
return true;

this.skipRenderAfterSplitterChange = false;
return false;
}

#endregion

private string WorkspaceSidebarToggleIcon => this.SettingsManager.ConfigurationData.Workspace.IsSidebarVisible ? Icons.Material.Filled.ArrowCircleLeft : Icons.Material.Filled.ArrowCircleRight;
Expand Down Expand Up @@ -75,6 +110,7 @@ private void SplitterChanged(double position)
this.splitterPosition = position;
this.splitterSaveTimer.Stop();
this.splitterSaveTimer.Start();
this.skipRenderAfterSplitterChange = true;
}

private void ToggleWorkspacesOverlay()
Expand Down
Loading