feat(ui): add operation-timing NDJSON sink for hub invokes and bulk DB paths

Two chokepoints per P0-2: WorkerClient.InvokeTimedAsync(<T>) wraps all 63
_hub.InvokeAsync call sites (plus TryInvokeAsync), and Stopwatch blocks
around the Island VMs' bulk DB load/write paths (list load, task list load,
reorders, batch delete, reconcile tick, count refresh). Single-row reads
stay untouched per the plan's rule of thumb. No display, no behavior change.
This commit is contained in:
Mika Kuns
2026-08-12 08:31:35 +02:00
parent eca16a3506
commit a614feb96d
6 changed files with 309 additions and 105 deletions
@@ -360,6 +360,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
private async Task LoadForListAsync(
ListNavItemViewModel list, CancellationToken ct, Dictionary<string, TaskRowViewModel>? reusable)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var ok = false;
try
{
await using var db = await _dbFactory.CreateDbContextAsync(ct);
@@ -460,8 +462,10 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
Regroup();
UpdateSubtitle();
ok = true;
}
catch (OperationCanceledException) { }
finally { OperationTiming.Shared.Record("db", "TasksIsland.LoadForListAsync", sw.Elapsed, ok); }
}
internal void Regroup()
@@ -816,17 +820,24 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
var listId = _currentList.Id["user:".Length..];
var orderedIds = Items.Select(i => i.Id).ToList();
await using var db = await _dbFactory.CreateDbContextAsync();
var idSet = orderedIds.ToHashSet();
var entities = await db.Tasks
.Where(t => t.ListId == listId && idSet.Contains(t.Id))
.ToListAsync();
for (int i = 0; i < orderedIds.Count; i++)
var sw = System.Diagnostics.Stopwatch.StartNew();
var ok = false;
try
{
var e = entities.FirstOrDefault(x => x.Id == orderedIds[i]);
if (e is not null) e.SortOrder = i;
await using var db = await _dbFactory.CreateDbContextAsync();
var idSet = orderedIds.ToHashSet();
var entities = await db.Tasks
.Where(t => t.ListId == listId && idSet.Contains(t.Id))
.ToListAsync();
for (int i = 0; i < orderedIds.Count; i++)
{
var e = entities.FirstOrDefault(x => x.Id == orderedIds[i]);
if (e is not null) e.SortOrder = i;
}
await db.SaveChangesAsync();
ok = true;
}
await db.SaveChangesAsync();
finally { OperationTiming.Shared.Record("db", "TasksIsland.ReorderAsync", sw.Elapsed, ok); }
}
private static void MoveWithinCollection<T>(
@@ -990,17 +1001,22 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
if (!ok) return;
}
await using var db = await _dbFactory.CreateDbContextAsync();
var repo = new TaskRepository(db);
foreach (var row in toDelete)
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
try
await using var db = await _dbFactory.CreateDbContextAsync();
var repo = new TaskRepository(db);
foreach (var row in toDelete)
{
await repo.DeleteAsync(row.Id);
Items.Remove(row);
try
{
await repo.DeleteAsync(row.Id);
Items.Remove(row);
}
catch { /* still referenced by open child tasks; leave it visible */ }
}
catch { /* still referenced by open child tasks; leave it visible */ }
}
finally { OperationTiming.Shared.Record("db", "TasksIsland.ClearCompletedAsync", sw.Elapsed, ok: true); }
Regroup();
UpdateSubtitle();
@@ -1476,6 +1492,8 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
}
List<TaskEntity> entities;
var sw = System.Diagnostics.Stopwatch.StartNew();
var dbOk = false;
try
{
var idSet = ids.ToHashSet();
@@ -1485,6 +1503,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
.Include(t => t.Worktree)
.Where(t => idSet.Contains(t.Id))
.ToListAsync(ct);
dbOk = true;
}
catch (OperationCanceledException) { return; }
catch (Exception ex)
@@ -1492,6 +1511,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
System.Diagnostics.Debug.WriteLine($"TasksIsland: reconcile tick failed ({ex.Message})");
return;
}
finally { OperationTiming.Shared.Record("db", "TasksIsland.ReconcileTickAsync", sw.Elapsed, dbOk); }
if (ReconcileTickTestBarrier is { } barrier) await barrier();