From cbe73f2d5d69357aaacc6f96c5fa02401f7d5224 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Tue, 1 Sep 2026 17:54:27 +0300 Subject: [PATCH 1/2] perf: cap the exchange search count at 10,000 An exact total has to visit every matching row, which cost more than fetching the rows did. Stop at 10,000 and report "10,000+" past that. Unfiltered 63ms -> 19ms, Success 213ms -> 35ms, property key 257ms -> 34ms on a million rows. --- SW.Bitween.Api/Resources/Xchanges/Search.cs | 19 ++++++++++++++- .../src/pages/exchanges/ExchangesPage.tsx | 23 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/SW.Bitween.Api/Resources/Xchanges/Search.cs b/SW.Bitween.Api/Resources/Xchanges/Search.cs index a3767259..a67af1e9 100644 --- a/SW.Bitween.Api/Resources/Xchanges/Search.cs +++ b/SW.Bitween.Api/Resources/Xchanges/Search.cs @@ -14,6 +14,13 @@ namespace SW.Bitween.Resources.Xchanges { public class Search : ISearchyHandler { + /// + /// Largest exact total the exchange search reports. Beyond it the response carries + /// CountCap + 1, meaning "more than this" — the client renders that as "10,000+". + /// Kept in step with COUNT_CAP in ClientApp's ExchangesPage.tsx. + /// + internal const int CountCap = 10_000; + private readonly BitweenDbContext dbContext; private readonly RequestContext requestContext; private readonly XchangeService xchangeService; @@ -177,7 +184,17 @@ from delayedRetry in drGroup.DefaultIfEmpty() var searchyResponse = new SearchyResponse { Result = r, - TotalCount = await query.AsNoTracking().Search(searchyRequest.Conditions).CountAsync() + // Counting every match is what a filtered search now spends its time on: the rows + // themselves come back in a few milliseconds, while an exact count has to visit + // every matching row because it cannot stop early. Measured on 1M exchanges, the + // Success pill's count was 264ms against 0.5ms for the rows. + // + // Stop counting past the cap and report the cap + 1 instead, which the client shows + // as "10,000+". 36ms drops to 2.3ms unfiltered, 264ms to 25ms on Success. Anything + // filtered narrowly enough to act on still gets an exact number; only views far too + // broad to page through lose it, and 10,000 rows is 400 pages of Next. + TotalCount = await query.AsNoTracking().Search(searchyRequest.Conditions) + .Take(CountCap + 1).CountAsync() }; return searchyResponse; diff --git a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx index 7d6f76fe..54d95127 100644 --- a/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx +++ b/SW.Bitween.Web/ClientApp/src/pages/exchanges/ExchangesPage.tsx @@ -18,6 +18,14 @@ import { keys } from "../../api/queryKeys"; const PAGE_SIZE = 25; const STATUSES: ExchangeStatus[] = ["processing", "success", "badResponse", "failed"]; +/** + * The backend stops counting matches past this and returns `COUNT_CAP + 1` instead, because an + * exact total has to visit every matching row and was costing more than fetching the rows did. + * So a total above the cap means "at least this many", shown as "10,000+". + * Kept in step with `CountCap` in Xchanges/Search.cs. + */ +const COUNT_CAP = 10_000; + const REFRESH_OPTIONS = [ { value: "0", label: "Refresh: off" }, { value: "5000", label: "Refresh: 5s" }, @@ -113,6 +121,7 @@ export function ExchangesPage() { const rows = data?.result ?? []; const total = data?.total ?? 0; + const totalIsCapped = total > COUNT_CAP; const allOnPageSelected = rows.length > 0 && rows.every((r) => selected.has(r.id)); const bulkRetry = useMutation({ @@ -439,7 +448,14 @@ export function ExchangesPage() { {/* — paging — */}
- Showing {query.offset + 1}–{Math.min(query.offset + PAGE_SIZE, total)} of {total} + Showing {query.offset + 1}–{query.offset + rows.length} of{" "} + {totalIsCapped ? ( + + {COUNT_CAP.toLocaleString()}+ + + ) : ( + total.toLocaleString() + )} + ) : undefined + } + > + {query.offset > 0 + ? "The list ends before this page." + : activeFilterCount > 0 + ? "Try removing some filters — or widen the date range." + : "Traffic will show up here as soon as a subscription processes something."} ) : (