Perf: debounce Insights filter + stats so search-typing doesn't restall the UI#13
Draft
mimeding wants to merge 2 commits into
Draft
Perf: debounce Insights filter + stats so search-typing doesn't restall the UI#13mimeding wants to merge 2 commits into
mimeding wants to merge 2 commits into
Conversation
…tall UI
The Insights tab computed two derived values from the log ring buffer
on every SwiftUI body invalidation:
* filteredLogs: O(N) filter pass, with N up to 500 and fuzzy-match
inside SearchService.matches for each log.
* stats: another set of O(N) scans (success / error / inference
counts, plus token sums and tps averages).
Both were called as computed properties from the view, so each
keystroke in the search field re-ran the full filter and the full
stats computation on the main actor. On a busy HTTP server the same
recompute happens whenever a new log is inserted via the
nonisolated logRequest hop — multiple times a second during a
streaming response.
Move the heavy work onto a Combine pipeline:
* logs is throttled at 50ms so a burst of inserts coalesces.
* searchFilter is debounced at 150ms so the filter only runs after
the user pauses typing.
* source / method filters propagate immediately (no typing latency).
A CombineLatest4 of those four publishers maps to (filteredLogs,
stats) and assigns the result to two new @published caches —
displayedLogs and displayedStats — that the view observes directly.
The legacy computed filteredLogs and stats stay (now thin wrappers
over the same static helpers) so any other reader keeps working.
InsightsView is updated to read displayedLogs / displayedStats. The
log-row ForEach also caches displayedLogs in a local so each row
doesn't trigger an extra property read for the divider lookup.
Adds InsightsServiceFilteringTests covering the pure-function
helpers (empty input, source / method isolation, stats success +
error counts, duration averaging, inference token sums). Tests use
the static helpers directly without touching the MainActor singleton
or Combine, so they run cleanly in any test runner.
Co-authored-by: Michael Meding <mimeding@users.noreply.github.com>
9 tasks
ModelManager.init kicks off an unstructured Task that calls loadOsaurusAIOrgModels(), which fetches the OsaurusAI organization listing from Hugging Face and feeds the result through applyOsaurusOrgFetch. The unit-test runner repeatedly constructs ModelManager() to drive applyOsaurusOrgFetch directly. The background launch-time fetch races with those test calls — whichever finishes last wins, and the merge result is non-deterministic. That's the root cause of the flaky ModelManagerSuggestedTests failures seen across many of the recent PR CI runs (applyOsaurusOrgFetch_dropsStaleAutoFetched OnReapply, applyOsaurusOrgFetch_addsNewEntriesAfterCurated, etc.). Gate the launch-time fetch on a small isRunningInTestEnvironment helper that checks for any of XCTestConfigurationFilePath, XCTestBundlePath, or XCTestSessionIdentifier in the process environment. Those variables are only present inside an xctest host process; production app launches still get the HF fetch exactly as before. This is a network call, so removing it under tests also has the side benefit of making the test suite work offline / on hermetic CI runners. Co-authored-by: Michael Meding <mimeding@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Why this matters (business)
The Insights tab is the operator-facing view for the local API. Today, opening it during real traffic — a streaming HTTP run, a busy plugin, a quick
curlloop — causes the search field to feel sticky on every keystroke and the stats bar to wobble as requests come in. The bottleneck is that every keystroke triggers an O(N) filter pass over up to 500 logs (with fuzzy matching inside each log's path/model/plugin fields), and a separate O(N) scan to recompute stats. Both run synchronously on the main actor on every SwiftUI body invalidation. New incoming requests amplify the problem because every nonisolatedlogRequestinsertion bumps@Published var logs, which invalidates the view, which re-runs both computations.This makes the tab look broken precisely when it's most useful (under load) and erodes operator trust that the local server is healthy.
What's wrong (technical)
/// Filtered logs based on current filter settings var filteredLogs: [RequestLog] { logs.filter { log in // Search filter (path, model, or pluginId) using fuzzy matching ... } } /// Summary statistics var stats: InsightsStats { let total = logs.count let successCount = logs.filter { $0.isSuccess }.count ... let inferenceLogs = logs.filter { $0.isInference } ... }Both are computed properties, evaluated on every
bodyread inInsightsView. The view binds the search field to a@Published var searchFilter, so every typed character causes a@Publishedchange → view invalidation → recompute.Fix
Refactor the heavy work into a Combine pipeline that runs on a background-of-main schedule rather than synchronously per-body:
nonisolated statichelpers (applyFilters,computeStats) so they can be reused and tested without the singleton.init(), wireCombineLatest4($logs, $searchFilter, $sourceFilter, $methodFilter)with:$logsthrottled 50ms (coalesce bursty insertion from streaming traffic);$searchFilterdebounced 150ms (only re-filter after the user pauses typing);removeDuplicates.(filteredLogs, stats)and assigns the result to two new@Published private(set) var displayedLogs/displayedStats.InsightsViewnow readsdisplayedLogsanddisplayedStats. TheForEachalso cachesdisplayedLogsinto a local so each row doesn't double-index the property for the divider check.The legacy computed
filteredLogsandstatsstay (now thin wrappers over the same static helpers) so any other caller — including tests — keeps compiling.Tests
InsightsServiceFilteringTestscovers the pure-function helpers:sourceFilterisolates byRequestSource.methodFilterisolates by HTTP method.computeStats([])returns zeroed stats.computeStatscounts success/error correctly (200,204are success;500with errorMessage is error).computeStatsaverages duration.computeStatssums inference tokens only from/chat-pathed logs.Tests touch only the static helpers and don't spin up Combine or the MainActor singleton, so they run cleanly anywhere.
Scope decisions
searchDebounceMs,recomputeDebounceMs) so they're easy to tune.filteredLogs/statsproperties. They're still exported as part of the InsightsService surface and other code (or future code) may legitimately want a synchronous read. The Combine-drivendisplayedLogsis purely additive.Task { @MainActor in shared.log(log) }hop; coalescing those bursts is what the throttle on$logsdoes in this PR.Changes
InsightsServiceFilteringTests)Test Plan
hey -n 200 -c 8 -m POST -D body.json http://localhost:1337/v1/chat/completionsor similar). Open Insights mid-flight.cd Packages/OsaurusCore && swift test --filter InsightsServiceFilteringTestspasses.Checklist
CONTRIBUTING.mdswiftc -frontend -parse)