Skip to content

Commit f3c5190

Browse files
Merge pull request #1110 from erikdarlingdata/feature/perf-lite-offthread
Perf: move Lite background pipeline + full refresh off the UI thread (P1)
2 parents d21f014 + fd367d5 commit f3c5190

2 files changed

Lines changed: 51 additions & 39 deletions

File tree

Lite/Controls/ServerTab.Refresh.cs

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -127,34 +127,39 @@ private async System.Threading.Tasks.Task RefreshAllTabsAsync(int hoursBack, Dat
127127
{
128128
var loadSw = Stopwatch.StartNew();
129129

130-
/* Load all tabs in parallel */
131-
var snapshotsTask = _dataService.GetLatestQuerySnapshotsAsync(_serverId, hoursBack, fromDate, toDate);
132-
var cpuTask = _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate);
133-
var memoryTask = _dataService.GetLatestMemoryStatsAsync(_serverId);
134-
var memoryTrendTask = _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate);
135-
var queryStatsTask = _dataService.GetTopQueriesByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes);
136-
var procStatsTask = _dataService.GetTopProceduresByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes);
137-
var fileIoTrendTask = _dataService.GetFileIoLatencyTrendAsync(_serverId, hoursBack, fromDate, toDate);
138-
var fileIoThroughputTask = _dataService.GetFileIoThroughputTrendAsync(_serverId, hoursBack, fromDate, toDate);
139-
var tempDbTask = _dataService.GetTempDbTrendAsync(_serverId, hoursBack, fromDate, toDate);
140-
var tempDbFileIoTask = _dataService.GetTempDbFileIoTrendAsync(_serverId, hoursBack, fromDate, toDate);
141-
var deadlockTask = _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate);
142-
var blockedProcessTask = _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate);
143-
var waitTypesTask = _dataService.GetDistinctWaitTypesAsync(_serverId, hoursBack, fromDate, toDate);
144-
var memoryClerkTypesTask = _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate);
145-
var perfmonCountersTask = _dataService.GetDistinctPerfmonCountersAsync(_serverId, hoursBack, fromDate, toDate);
146-
var queryStoreTask = _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate);
147-
var memoryGrantTrendTask = _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate);
148-
var memoryGrantChartTask = _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate);
149-
var memoryPressureEventsTask = _dataService.GetMemoryPressureEventsAsync(_serverId, hoursBack, fromDate, toDate);
150-
var serverConfigTask = SafeQueryAsync(() => _dataService.GetLatestServerConfigAsync(_serverId));
151-
var databaseConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseConfigAsync(_serverId));
152-
var databaseScopedConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseScopedConfigAsync(_serverId));
153-
var traceFlagsTask = SafeQueryAsync(() => _dataService.GetLatestTraceFlagsAsync(_serverId));
154-
var runningJobsTask = SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId));
155-
var collectionHealthTask = SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId));
156-
var collectionLogTask = SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack));
157-
var dailySummaryTask = _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate);
130+
/* Load all tabs in parallel, off the UI thread.
131+
DuckDB.NET is synchronous, so calling these directly on the dispatcher would run all
132+
~36 queries serially on the UI thread (the freeze). Task.Run pushes each onto a pool
133+
thread — each query's read lock is acquired AND released inside that one synchronous run,
134+
so the ReaderWriterLockSlim thread affinity is respected — and the fan-out below becomes
135+
genuinely parallel. The UI-update code after the awaits is unchanged. */
136+
var snapshotsTask = Task.Run(() => _dataService.GetLatestQuerySnapshotsAsync(_serverId, hoursBack, fromDate, toDate));
137+
var cpuTask = Task.Run(() => _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate));
138+
var memoryTask = Task.Run(() => _dataService.GetLatestMemoryStatsAsync(_serverId));
139+
var memoryTrendTask = Task.Run(() => _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate));
140+
var queryStatsTask = Task.Run(() => _dataService.GetTopQueriesByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes));
141+
var procStatsTask = Task.Run(() => _dataService.GetTopProceduresByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes));
142+
var fileIoTrendTask = Task.Run(() => _dataService.GetFileIoLatencyTrendAsync(_serverId, hoursBack, fromDate, toDate));
143+
var fileIoThroughputTask = Task.Run(() => _dataService.GetFileIoThroughputTrendAsync(_serverId, hoursBack, fromDate, toDate));
144+
var tempDbTask = Task.Run(() => _dataService.GetTempDbTrendAsync(_serverId, hoursBack, fromDate, toDate));
145+
var tempDbFileIoTask = Task.Run(() => _dataService.GetTempDbFileIoTrendAsync(_serverId, hoursBack, fromDate, toDate));
146+
var deadlockTask = Task.Run(() => _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate));
147+
var blockedProcessTask = Task.Run(() => _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate));
148+
var waitTypesTask = Task.Run(() => _dataService.GetDistinctWaitTypesAsync(_serverId, hoursBack, fromDate, toDate));
149+
var memoryClerkTypesTask = Task.Run(() => _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate));
150+
var perfmonCountersTask = Task.Run(() => _dataService.GetDistinctPerfmonCountersAsync(_serverId, hoursBack, fromDate, toDate));
151+
var queryStoreTask = Task.Run(() => _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate));
152+
var memoryGrantTrendTask = Task.Run(() => _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate));
153+
var memoryGrantChartTask = Task.Run(() => _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate));
154+
var memoryPressureEventsTask = Task.Run(() => _dataService.GetMemoryPressureEventsAsync(_serverId, hoursBack, fromDate, toDate));
155+
var serverConfigTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetLatestServerConfigAsync(_serverId)));
156+
var databaseConfigTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetLatestDatabaseConfigAsync(_serverId)));
157+
var databaseScopedConfigTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetLatestDatabaseScopedConfigAsync(_serverId)));
158+
var traceFlagsTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetLatestTraceFlagsAsync(_serverId)));
159+
var runningJobsTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId)));
160+
var collectionHealthTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId)));
161+
var collectionLogTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack)));
162+
var dailySummaryTask = Task.Run(() => _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate));
158163
/* Core data tasks */
159164
await System.Threading.Tasks.Task.WhenAll(
160165
snapshotsTask, cpuTask, memoryTask, memoryTrendTask,
@@ -165,15 +170,15 @@ await System.Threading.Tasks.Task.WhenAll(
165170
runningJobsTask, collectionHealthTask, collectionLogTask, dailySummaryTask);
166171

167172
/* Trend chart tasks - run separately so failures don't kill the whole refresh */
168-
var lockWaitTrendTask = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate));
169-
var blockingTrendTask = SafeQueryAsync(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate));
170-
var deadlockTrendTask = SafeQueryAsync(() => _dataService.GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate));
171-
var queryDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryDurationTrendAsync(_serverId, hoursBack, fromDate, toDate));
172-
var procDurationTrendTask = SafeQueryAsync(() => _dataService.GetProcedureDurationTrendAsync(_serverId, hoursBack, fromDate, toDate));
173-
var queryStoreDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryStoreDurationTrendAsync(_serverId, hoursBack, fromDate, toDate));
174-
var executionCountTrendTask = SafeQueryAsync(() => _dataService.GetExecutionCountTrendAsync(_serverId, hoursBack, fromDate, toDate));
175-
var currentWaitsDurationTask = SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate));
176-
var currentWaitsBlockedTask = SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate));
173+
var lockWaitTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate)));
174+
var blockingTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate)));
175+
var deadlockTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate)));
176+
var queryDurationTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetQueryDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)));
177+
var procDurationTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetProcedureDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)));
178+
var queryStoreDurationTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetQueryStoreDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)));
179+
var executionCountTrendTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetExecutionCountTrendAsync(_serverId, hoursBack, fromDate, toDate)));
180+
var currentWaitsDurationTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate)));
181+
var currentWaitsBlockedTask = Task.Run(() => SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate)));
177182

178183
await System.Threading.Tasks.Task.WhenAll(
179184
lockWaitTrendTask, blockingTrendTask, deadlockTrendTask,

Lite/MainWindow.xaml.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,16 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
169169
analysisNotificationService,
170170
new AppLoggerAdapter<CollectionBackgroundService>());
171171

172-
// Start background collection
172+
// Start background collection.
173+
// Off the UI thread on purpose: DuckDB.NET is synchronous and Lite has no
174+
// ConfigureAwait(false), so starting this from the Loaded handler would run the entire
175+
// collection/checkpoint/archive pipeline on the WPF dispatcher (per-minute jank, and a
176+
// multi-second-to-minutes freeze on archive/reset). A pool thread has no
177+
// SynchronizationContext, so StartAsync and every subsequent continuation stay off-UI.
178+
// Safe: the pipeline only touches DuckDB + the email/webhook notification service; the
179+
// UI reads data by polling DuckDB on its own timers, fully decoupled.
173180
_backgroundCts = new CancellationTokenSource();
174-
_ = _backgroundService.StartAsync(_backgroundCts.Token);
181+
_ = Task.Run(() => _backgroundService.StartAsync(_backgroundCts.Token));
175182

176183
// Initialize system tray
177184
_trayService = new SystemTrayService(this, RestoreFromTray, _backgroundService);

0 commit comments

Comments
 (0)