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:
@@ -610,6 +610,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
private async System.Threading.Tasks.Task BindAsync(TaskRowViewModel row, CancellationToken ct)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
@@ -701,12 +703,16 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
}
|
||||
await Merge.RefreshMergePreviewAsync();
|
||||
ok = true;
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
finally { OperationTiming.Shared.Record("db", "DetailsIsland.BindAsync", sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task LoadChildOutcomesAsync(string parentTaskId, CancellationToken ct)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
@@ -717,6 +723,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ok = true;
|
||||
if (children.Count == 0) return;
|
||||
|
||||
ChildOutcomes.Clear();
|
||||
@@ -749,10 +756,13 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { /* best-effort */ }
|
||||
finally { OperationTiming.Shared.Record("db", "DetailsIsland.LoadChildOutcomesAsync", sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task LoadPlanningChildrenAsync(string parentTaskId, CancellationToken ct)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
@@ -762,6 +772,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
.Where(t => t.ParentTaskId == parentTaskId)
|
||||
.ToListAsync(ct);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
ok = true;
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
@@ -791,6 +802,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
catch { /* best-effort */ }
|
||||
finally { OperationTiming.Shared.Record("db", "DetailsIsland.LoadPlanningChildrenAsync", sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task RefreshPlanningChildAsync(string childTaskId)
|
||||
|
||||
@@ -255,30 +255,37 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
};
|
||||
foreach (var s in smart) { Items.Add(s); SmartLists.Add(s); }
|
||||
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var lists = new ListRepository(ctx);
|
||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||
var dotColors = new[] { "Moss", "Peat", "Sage" };
|
||||
int idx = 0;
|
||||
foreach (var l in await lists.GetAllAsync(ct))
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
if (seedNames.Contains(l.Name)) continue;
|
||||
var item = new ListNavItemViewModel
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var lists = new ListRepository(ctx);
|
||||
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
|
||||
var dotColors = new[] { "Moss", "Peat", "Sage" };
|
||||
int idx = 0;
|
||||
foreach (var l in await lists.GetAllAsync(ct))
|
||||
{
|
||||
Id = $"user:{l.Id}",
|
||||
Name = l.Name,
|
||||
Kind = ListKind.User,
|
||||
IconKey = "Folder",
|
||||
DotColorKey = dotColors[idx % dotColors.Length],
|
||||
WorkingDir = l.WorkingDir,
|
||||
DefaultCommitType = l.DefaultCommitType,
|
||||
IsManual = l.IsManual,
|
||||
FindingsTracked = l.FindingsTracked,
|
||||
};
|
||||
Items.Add(item);
|
||||
UserLists.Add(item);
|
||||
idx++;
|
||||
if (seedNames.Contains(l.Name)) continue;
|
||||
var item = new ListNavItemViewModel
|
||||
{
|
||||
Id = $"user:{l.Id}",
|
||||
Name = l.Name,
|
||||
Kind = ListKind.User,
|
||||
IconKey = "Folder",
|
||||
DotColorKey = dotColors[idx % dotColors.Length],
|
||||
WorkingDir = l.WorkingDir,
|
||||
DefaultCommitType = l.DefaultCommitType,
|
||||
IsManual = l.IsManual,
|
||||
FindingsTracked = l.FindingsTracked,
|
||||
};
|
||||
Items.Add(item);
|
||||
UserLists.Add(item);
|
||||
idx++;
|
||||
}
|
||||
ok = true;
|
||||
}
|
||||
finally { OperationTiming.Shared.Record("db", "ListsIsland.LoadAsync", sw.Elapsed, ok); }
|
||||
|
||||
await RefreshCountsAsync(ct);
|
||||
SelectedList = Items.FirstOrDefault();
|
||||
@@ -286,6 +293,8 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
public async Task RefreshCountsAsync(CancellationToken ct = default)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||
@@ -306,9 +315,11 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
var filter = _filters.Resolve(item.Id);
|
||||
item.Count = filter is null ? 0 : all.Count(filter.ShouldCount);
|
||||
}
|
||||
ok = true;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch { /* best-effort refresh */ }
|
||||
finally { OperationTiming.Shared.Record("db", "ListsIsland.RefreshCountsAsync", sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -382,9 +393,16 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
MoveWithinCollection(UserLists, source, target, placeBelow);
|
||||
|
||||
var orderedIds = UserLists.Select(i => i.Id["user:".Length..]).ToList();
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var lists = new ListRepository(ctx);
|
||||
await lists.ReorderAsync(orderedIds);
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||
var lists = new ListRepository(ctx);
|
||||
await lists.ReorderAsync(orderedIds);
|
||||
ok = true;
|
||||
}
|
||||
finally { OperationTiming.Shared.Record("db", "ListsIsland.ReorderAsync", sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
private static void MoveWithinCollection(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user