feat(claude-do): merge [P0-2] Timing-Hook für Hub-Invokes und Island-DB-Pfad
ClaudeDo-Task: a96c2c16-62e0-48f2-b80d-d98b113c7583
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using System.Text.Json;
|
||||
using ClaudeDo.Data;
|
||||
|
||||
namespace ClaudeDo.Ui.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Appends one NDJSON line per timed operation (hub invoke, bulk DB read/write) to a log file, so
|
||||
/// a day of normal usage yields a file that can be sorted by duration to find real outliers.
|
||||
/// Never throws — a failed write is swallowed, because a measurement must never disturb the app.
|
||||
/// </summary>
|
||||
public sealed class OperationTiming
|
||||
{
|
||||
public static string DefaultPath =>
|
||||
Path.Combine(Paths.AppDataRoot(), "logs", "operation-timing.ndjson");
|
||||
|
||||
public static OperationTiming Shared { get; } = new(DefaultPath);
|
||||
|
||||
private readonly string _path;
|
||||
private readonly object _writeLock = new();
|
||||
|
||||
public OperationTiming(string filePath) => _path = filePath;
|
||||
|
||||
public void Record(string kind, string operation, TimeSpan elapsed, bool ok)
|
||||
{
|
||||
try
|
||||
{
|
||||
var line = JsonSerializer.Serialize(new
|
||||
{
|
||||
ts = DateTime.UtcNow,
|
||||
kind,
|
||||
op = operation,
|
||||
ms = (long)elapsed.TotalMilliseconds,
|
||||
ok,
|
||||
});
|
||||
|
||||
lock (_writeLock)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(_path);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.AppendAllText(_path, line + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A measurement must never disturb the app.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,23 +277,58 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
/// <summary>Invoke a hub method, returning default (null) when the worker is offline or errors.</summary>
|
||||
private async Task<T?> TryInvokeAsync<T>(string method, params object?[] args)
|
||||
{
|
||||
try { return await _hub.InvokeCoreAsync<T>(method, args); }
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
var result = await _hub.InvokeCoreAsync<T>(method, args);
|
||||
ok = true;
|
||||
return result;
|
||||
}
|
||||
catch { return default; }
|
||||
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
/// <summary>Times a void hub invoke without changing its exception/cancellation behavior.</summary>
|
||||
private async Task InvokeTimedAsync(string method, Func<Task> invoke)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
await invoke();
|
||||
ok = true;
|
||||
}
|
||||
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
/// <summary>Times a hub invoke that returns a value, without changing its exception/cancellation behavior.</summary>
|
||||
private async Task<T> InvokeTimedAsync<T>(string method, Func<Task<T>> invoke)
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var ok = false;
|
||||
try
|
||||
{
|
||||
var result = await invoke();
|
||||
ok = true;
|
||||
return result;
|
||||
}
|
||||
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
||||
}
|
||||
|
||||
public async Task RunNowAsync(string taskId)
|
||||
{
|
||||
await _hub.InvokeAsync("RunNow", taskId);
|
||||
await InvokeTimedAsync("RunNow", () => _hub.InvokeAsync("RunNow", taskId));
|
||||
}
|
||||
|
||||
public async Task ContinueTaskAsync(string taskId, string followUpPrompt)
|
||||
{
|
||||
await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt);
|
||||
await InvokeTimedAsync("ContinueTask", () => _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt));
|
||||
}
|
||||
|
||||
public async Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer)
|
||||
{
|
||||
try { await _hub.InvokeAsync<bool>("AnswerTaskQuestion", taskId, questionId, answer); }
|
||||
try { await InvokeTimedAsync<bool>("AnswerTaskQuestion", () => _hub.InvokeAsync<bool>("AnswerTaskQuestion", taskId, questionId, answer)); }
|
||||
catch { /* offline or already resolved — the UI clears optimistically */ }
|
||||
}
|
||||
|
||||
@@ -302,43 +337,43 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task ResetTaskAsync(string taskId)
|
||||
{
|
||||
await _hub.InvokeAsync("ResetTask", taskId);
|
||||
await InvokeTimedAsync("ResetTask", () => _hub.InvokeAsync("ResetTask", taskId));
|
||||
}
|
||||
|
||||
public async Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage)
|
||||
{
|
||||
return await _hub.InvokeAsync<MergeResultDto>(
|
||||
"MergeTask", taskId, targetBranch, removeWorktree, commitMessage);
|
||||
return await InvokeTimedAsync<MergeResultDto>("MergeTask", () => _hub.InvokeAsync<MergeResultDto>(
|
||||
"MergeTask", taskId, targetBranch, removeWorktree, commitMessage));
|
||||
}
|
||||
|
||||
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
|
||||
=> _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch);
|
||||
=> InvokeTimedAsync<MergeResultDto>("StartConflictMerge", () => _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch));
|
||||
|
||||
public Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId)
|
||||
=> _hub.InvokeAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", taskId);
|
||||
=> InvokeTimedAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", () => _hub.InvokeAsync<MergeConflictDocumentsDto>("GetMergeConflictDocuments", taskId));
|
||||
|
||||
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
|
||||
=> _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent);
|
||||
=> InvokeTimedAsync("WriteConflictResolution", () => _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent));
|
||||
|
||||
public Task<MergeResultDto> ContinueConflictMergeAsync(string taskId)
|
||||
=> _hub.InvokeAsync<MergeResultDto>("ContinueConflictMerge", taskId);
|
||||
=> InvokeTimedAsync<MergeResultDto>("ContinueConflictMerge", () => _hub.InvokeAsync<MergeResultDto>("ContinueConflictMerge", taskId));
|
||||
|
||||
public Task AbortConflictMergeAsync(string taskId)
|
||||
=> _hub.InvokeAsync("AbortConflictMerge", taskId);
|
||||
=> InvokeTimedAsync("AbortConflictMerge", () => _hub.InvokeAsync("AbortConflictMerge", taskId));
|
||||
|
||||
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
|
||||
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
|
||||
|
||||
public async Task CancelTaskAsync(string taskId)
|
||||
{
|
||||
await _hub.InvokeAsync("CancelTask", taskId);
|
||||
await InvokeTimedAsync("CancelTask", () => _hub.InvokeAsync("CancelTask", taskId));
|
||||
}
|
||||
|
||||
public async Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hub.InvokeAsync("DeleteTask", taskId, CancellationToken.None);
|
||||
await InvokeTimedAsync("DeleteTask", () => _hub.InvokeAsync("DeleteTask", taskId, CancellationToken.None));
|
||||
return (true, null);
|
||||
}
|
||||
catch (HubException ex)
|
||||
@@ -354,7 +389,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task WakeQueueAsync()
|
||||
{
|
||||
await _hub.InvokeAsync("WakeQueue");
|
||||
await InvokeTimedAsync("WakeQueue", () => _hub.InvokeAsync("WakeQueue"));
|
||||
}
|
||||
|
||||
public async Task<List<AgentInfo>> GetAgentsAsync()
|
||||
@@ -362,17 +397,17 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task RefreshAgentsAsync()
|
||||
{
|
||||
await _hub.InvokeAsync("RefreshAgents");
|
||||
await InvokeTimedAsync("RefreshAgents", () => _hub.InvokeAsync("RefreshAgents"));
|
||||
}
|
||||
|
||||
public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
||||
=> await _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
||||
=> await InvokeTimedAsync<SeedResultDto>("RestoreDefaultAgents", () => _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents"));
|
||||
|
||||
private async Task SeedActiveTasksAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var active = await _hub.InvokeAsync<List<ActiveTaskDto>>("GetActive");
|
||||
var active = await InvokeTimedAsync<List<ActiveTaskDto>>("GetActive", () => _hub.InvokeAsync<List<ActiveTaskDto>>("GetActive"));
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ActiveTasks.Clear();
|
||||
@@ -402,18 +437,18 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task UpdateAppSettingsAsync(AppSettingsDto dto)
|
||||
{
|
||||
await _hub.InvokeAsync("UpdateAppSettings", dto);
|
||||
await InvokeTimedAsync("UpdateAppSettings", () => _hub.InvokeAsync("UpdateAppSettings", dto));
|
||||
}
|
||||
|
||||
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
|
||||
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
|
||||
|
||||
public async Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
||||
=> await _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
||||
=> await InvokeTimedAsync<PrimeScheduleDto>("UpsertPrimeSchedule", () => _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto));
|
||||
|
||||
public async Task DeletePrimeScheduleAsync(Guid id)
|
||||
{
|
||||
try { await _hub.InvokeAsync("DeletePrimeSchedule", id); }
|
||||
try { await InvokeTimedAsync("DeletePrimeSchedule", () => _hub.InvokeAsync("DeletePrimeSchedule", id)); }
|
||||
catch { /* offline */ }
|
||||
}
|
||||
|
||||
@@ -423,27 +458,27 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
=> TryInvokeAsync<string>("GetWeekReport", IsoDay(start), IsoDay(end));
|
||||
|
||||
public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end)
|
||||
=> _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end));
|
||||
=> InvokeTimedAsync<string>("GenerateWeekReport", () => _hub.InvokeAsync<string>("GenerateWeekReport", IsoDay(start), IsoDay(end)));
|
||||
|
||||
public Task<bool> RunDailyPrepNowAsync()
|
||||
=> _hub.InvokeAsync<bool>("RunDailyPrepNow");
|
||||
=> InvokeTimedAsync<bool>("RunDailyPrepNow", () => _hub.InvokeAsync<bool>("RunDailyPrepNow"));
|
||||
|
||||
public Task RefineTaskAsync(string taskId) => _hub.InvokeAsync("RefineTask", taskId);
|
||||
public Task RefineTaskAsync(string taskId) => InvokeTimedAsync("RefineTask", () => _hub.InvokeAsync("RefineTask", taskId));
|
||||
|
||||
public Task ClearMyDayAsync()
|
||||
=> _hub.InvokeAsync("ClearMyDay");
|
||||
=> InvokeTimedAsync("ClearMyDay", () => _hub.InvokeAsync("ClearMyDay"));
|
||||
|
||||
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
||||
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
||||
|
||||
public async Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
||||
=> await _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
||||
=> await InvokeTimedAsync<DailyNoteDto>("AddDailyNote", () => _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text));
|
||||
|
||||
public async Task UpdateDailyNoteAsync(string id, string text)
|
||||
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
|
||||
=> await InvokeTimedAsync("UpdateDailyNote", () => _hub.InvokeAsync("UpdateDailyNote", id, text));
|
||||
|
||||
public async Task DeleteDailyNoteAsync(string id)
|
||||
=> await _hub.InvokeAsync("DeleteDailyNote", id);
|
||||
=> await InvokeTimedAsync("DeleteDailyNote", () => _hub.InvokeAsync("DeleteDailyNote", id));
|
||||
|
||||
public async Task<string> GetLastPrepLogAsync()
|
||||
=> await TryInvokeAsync<string>("GetLastPrepLog") ?? string.Empty;
|
||||
@@ -456,12 +491,12 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task UpdateListAsync(UpdateListDto dto)
|
||||
{
|
||||
await _hub.InvokeAsync("UpdateList", dto);
|
||||
await InvokeTimedAsync("UpdateList", () => _hub.InvokeAsync("UpdateList", dto));
|
||||
}
|
||||
|
||||
public async Task UpdateListConfigAsync(UpdateListConfigDto dto)
|
||||
{
|
||||
await _hub.InvokeAsync("UpdateListConfig", dto);
|
||||
await InvokeTimedAsync("UpdateListConfig", () => _hub.InvokeAsync("UpdateListConfig", dto));
|
||||
}
|
||||
|
||||
public Task<ListConfigDto?> GetListConfigAsync(string listId)
|
||||
@@ -469,37 +504,37 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto)
|
||||
{
|
||||
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
||||
await InvokeTimedAsync("UpdateTaskAgentSettings", () => _hub.InvokeAsync("UpdateTaskAgentSettings", dto));
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetRepoImportFoldersAsync()
|
||||
=> await _hub.InvokeAsync<List<string>>("GetRepoImportFolders");
|
||||
=> await InvokeTimedAsync<List<string>>("GetRepoImportFolders", () => _hub.InvokeAsync<List<string>>("GetRepoImportFolders"));
|
||||
|
||||
public Task SetRepoImportFoldersAsync(List<string> folders)
|
||||
=> _hub.InvokeAsync("SetRepoImportFolders", folders);
|
||||
=> InvokeTimedAsync("SetRepoImportFolders", () => _hub.InvokeAsync("SetRepoImportFolders", folders));
|
||||
|
||||
public async Task<List<SessionSkillDto>> GetSessionSkillsAsync()
|
||||
=> await TryInvokeAsync<List<SessionSkillDto>>("GetSessionSkills") ?? [];
|
||||
|
||||
public Task<List<string>> InstallSessionSkillAsync(string url)
|
||||
=> _hub.InvokeAsync<List<string>>("InstallSessionSkill", url);
|
||||
=> InvokeTimedAsync<List<string>>("InstallSessionSkill", () => _hub.InvokeAsync<List<string>>("InstallSessionSkill", url));
|
||||
|
||||
public Task UpdateSessionSkillAsync(string sourceUrl)
|
||||
=> _hub.InvokeAsync("UpdateSessionSkill", sourceUrl);
|
||||
=> InvokeTimedAsync("UpdateSessionSkill", () => _hub.InvokeAsync("UpdateSessionSkill", sourceUrl));
|
||||
|
||||
public Task RemoveSessionSkillAsync(string sourceUrl)
|
||||
=> _hub.InvokeAsync("RemoveSessionSkill", sourceUrl);
|
||||
=> InvokeTimedAsync("RemoveSessionSkill", () => _hub.InvokeAsync("RemoveSessionSkill", sourceUrl));
|
||||
|
||||
public async Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
|
||||
{
|
||||
var result = await _hub.InvokeAsync<SetTaskStatusResultDto>("SetTaskStatus", taskId, status.ToString());
|
||||
var result = await InvokeTimedAsync<SetTaskStatusResultDto>("SetTaskStatus", () => _hub.InvokeAsync<SetTaskStatusResultDto>("SetTaskStatus", taskId, status.ToString()));
|
||||
return result?.BaseDirty;
|
||||
}
|
||||
|
||||
public async Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch)
|
||||
{
|
||||
LastApproveTarget = targetBranch;
|
||||
return await _hub.InvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch);
|
||||
return await InvokeTimedAsync<MergeResultDto>("ApproveReview", () => _hub.InvokeAsync<MergeResultDto>("ApproveReview", taskId, targetBranch));
|
||||
}
|
||||
|
||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch)
|
||||
@@ -507,24 +542,24 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task RejectReviewToQueueAsync(string taskId, string feedback)
|
||||
{
|
||||
await _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback);
|
||||
await InvokeTimedAsync("RejectReviewToQueue", () => _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback));
|
||||
}
|
||||
|
||||
public async Task RejectReviewToIdleAsync(string taskId)
|
||||
{
|
||||
await _hub.InvokeAsync("RejectReviewToIdle", taskId);
|
||||
await InvokeTimedAsync("RejectReviewToIdle", () => _hub.InvokeAsync("RejectReviewToIdle", taskId));
|
||||
}
|
||||
|
||||
public async Task CancelReviewAsync(string taskId)
|
||||
{
|
||||
await _hub.InvokeAsync("CancelReview", taskId);
|
||||
await InvokeTimedAsync("CancelReview", () => _hub.InvokeAsync("CancelReview", taskId));
|
||||
}
|
||||
|
||||
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||
=> await _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||
=> await InvokeTimedAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", () => _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId));
|
||||
|
||||
public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
||||
=> await _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
||||
=> await InvokeTimedAsync<WorktreeResetDto>("ResetAllWorktrees", () => _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees"));
|
||||
|
||||
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
|
||||
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
|
||||
@@ -534,7 +569,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
{
|
||||
try
|
||||
{
|
||||
var ok = await _hub.InvokeAsync<bool>("SetWorktreeState", taskId, newState);
|
||||
var ok = await InvokeTimedAsync<bool>("SetWorktreeState", () => _hub.InvokeAsync<bool>("SetWorktreeState", taskId, newState));
|
||||
return (ok, null);
|
||||
}
|
||||
catch (HubException ex)
|
||||
@@ -548,63 +583,63 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
}
|
||||
|
||||
public async Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
||||
=> await _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
||||
=> await InvokeTimedAsync<ForceRemoveResultDto>("ForceRemoveWorktree", () => _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId));
|
||||
|
||||
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);
|
||||
=> await InvokeTimedAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", () => _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct));
|
||||
|
||||
public async Task<PlanningSessionResumeInfo> ResumePlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct);
|
||||
=> await InvokeTimedAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", () => _hub.InvokeAsync<PlanningSessionResumeInfo>("ResumePlanningSessionAsync", taskId, ct));
|
||||
|
||||
public async Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync("SubmitTaskForReview", taskId, ct);
|
||||
=> await InvokeTimedAsync("SubmitTaskForReview", () => _hub.InvokeAsync("SubmitTaskForReview", taskId, ct));
|
||||
|
||||
public async Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetInteractiveLaunchSpec", taskId, ct);
|
||||
=> await InvokeTimedAsync<LaunchSpec>("GetInteractiveLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetInteractiveLaunchSpec", taskId, ct));
|
||||
|
||||
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct);
|
||||
=> await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct));
|
||||
|
||||
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct);
|
||||
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, ct));
|
||||
|
||||
public async Task<string> CreateMergeHelperTaskAsync(
|
||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct);
|
||||
=> await InvokeTimedAsync<string>("CreateMergeHelperTask", () => _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct));
|
||||
|
||||
public async Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, nextPhase, ct);
|
||||
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, nextPhase, ct));
|
||||
|
||||
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
||||
=> await InvokeTimedAsync<LaunchSpec>("GetPlanningStartLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct));
|
||||
|
||||
public async Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", taskId, ct);
|
||||
=> await InvokeTimedAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetPlanningResumeLaunchSpec", taskId, ct));
|
||||
|
||||
public async Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct);
|
||||
=> await InvokeTimedAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", () => _hub.InvokeAsync<DiscardPlanningOutcome>("DiscardPlanningSessionAsync", taskId, dequeueQueuedChildren, ct));
|
||||
|
||||
public async Task<int> FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<int>("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct);
|
||||
=> await InvokeTimedAsync<int>("FinalizePlanningSessionAsync", () => _hub.InvokeAsync<int>("FinalizePlanningSessionAsync", taskId, queueAgentTasks, ct));
|
||||
|
||||
public async Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<int>("GetPendingDraftCountAsync", taskId, ct);
|
||||
=> await InvokeTimedAsync<int>("GetPendingDraftCountAsync", () => _hub.InvokeAsync<int>("GetPendingDraftCountAsync", taskId, ct));
|
||||
|
||||
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
||||
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
|
||||
|
||||
public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||
=> await _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||
=> await InvokeTimedAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", () => _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch));
|
||||
|
||||
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
||||
{
|
||||
await _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId);
|
||||
await InvokeTimedAsync("ContinuePlanningMerge", () => _hub.InvokeAsync("ContinuePlanningMerge", planningTaskId));
|
||||
}
|
||||
|
||||
public async Task AbortPlanningMergeAsync(string planningTaskId)
|
||||
{
|
||||
await _hub.InvokeAsync("AbortPlanningMerge", planningTaskId);
|
||||
await InvokeTimedAsync("AbortPlanningMerge", () => _hub.InvokeAsync("AbortPlanningMerge", planningTaskId));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync()
|
||||
@@ -613,20 +648,20 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
|
||||
public async Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default)
|
||||
{
|
||||
await _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct);
|
||||
await InvokeTimedAsync("QueuePlanningSubtasksAsync", () => _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct));
|
||||
}
|
||||
|
||||
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync()
|
||||
=> TryInvokeAsync<OnlineInboxStateDto>("GetOnlineInboxState");
|
||||
|
||||
public async Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input)
|
||||
=> await _hub.InvokeAsync("SetOnlineInboxConfig", input);
|
||||
=> await InvokeTimedAsync("SetOnlineInboxConfig", () => _hub.InvokeAsync("SetOnlineInboxConfig", input));
|
||||
|
||||
public async Task SetOnlineInboxAuthAsync(string refreshToken)
|
||||
=> await _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken);
|
||||
=> await InvokeTimedAsync("SetOnlineInboxAuth", () => _hub.InvokeAsync("SetOnlineInboxAuth", refreshToken));
|
||||
|
||||
public async Task ClearOnlineInboxAuthAsync()
|
||||
=> await _hub.InvokeAsync("ClearOnlineInboxAuth");
|
||||
=> await InvokeTimedAsync("ClearOnlineInboxAuth", () => _hub.InvokeAsync("ClearOnlineInboxAuth"));
|
||||
|
||||
public Task<UsageSnapshotDto?> GetUsageSnapshotAsync()
|
||||
=> TryInvokeAsync<UsageSnapshotDto>("GetUsageSnapshot");
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.Services;
|
||||
|
||||
public class OperationTimingTests
|
||||
{
|
||||
[Fact]
|
||||
public void Record_AppendsOneWellFormedNdjsonLinePerOperation()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), "claudedo_optiming_" + Guid.NewGuid().ToString("N"));
|
||||
var path = Path.Combine(dir, "operation-timing.ndjson");
|
||||
var sink = new OperationTiming(path);
|
||||
|
||||
try
|
||||
{
|
||||
sink.Record("hub", "RunNow", TimeSpan.FromMilliseconds(42), ok: true);
|
||||
sink.Record("db", "TasksIsland.LoadForList", TimeSpan.FromMilliseconds(7), ok: false);
|
||||
|
||||
var lines = File.ReadAllLines(path);
|
||||
Assert.Equal(2, lines.Length);
|
||||
|
||||
using var doc1 = JsonDocument.Parse(lines[0]);
|
||||
var root1 = doc1.RootElement;
|
||||
Assert.Equal("hub", root1.GetProperty("kind").GetString());
|
||||
Assert.Equal("RunNow", root1.GetProperty("op").GetString());
|
||||
Assert.Equal(42, root1.GetProperty("ms").GetInt64());
|
||||
Assert.True(root1.GetProperty("ok").GetBoolean());
|
||||
Assert.True(root1.TryGetProperty("ts", out _));
|
||||
|
||||
using var doc2 = JsonDocument.Parse(lines[1]);
|
||||
var root2 = doc2.RootElement;
|
||||
Assert.Equal("db", root2.GetProperty("kind").GetString());
|
||||
Assert.Equal("TasksIsland.LoadForList", root2.GetProperty("op").GetString());
|
||||
Assert.Equal(7, root2.GetProperty("ms").GetInt64());
|
||||
Assert.False(root2.GetProperty("ok").GetBoolean());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Record_SwallowsWriteFailureInsteadOfThrowing()
|
||||
{
|
||||
// A plain file sitting where the sink expects a directory makes Directory.CreateDirectory
|
||||
// throw -- standing in for the disk-full/locked-file/no-permission failures this sink
|
||||
// must survive without disturbing the caller.
|
||||
var root = Path.Combine(Path.GetTempPath(), "claudedo_optiming_invalid_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(root);
|
||||
var blockingFile = Path.Combine(root, "not-a-directory");
|
||||
File.WriteAllText(blockingFile, "");
|
||||
var targetPath = Path.Combine(blockingFile, "timing.ndjson");
|
||||
var sink = new OperationTiming(targetPath);
|
||||
|
||||
try
|
||||
{
|
||||
var exception = Record.Exception(() => sink.Record("hub", "RunNow", TimeSpan.FromMilliseconds(1), ok: true));
|
||||
|
||||
Assert.Null(exception);
|
||||
Assert.False(File.Exists(targetPath));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user