taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5d46295d1c23d022af1b3d4375860948f5b16c0a
parent 4c550543221e75f9cb59074bef72f4644f8a42e0
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 29 Jan 2026 16:01:22 +0100

wallet-core: refactor performance table to compact/sort on insertion

Diffstat:
Mpackages/taler-util/src/performance.ts | 109++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mpackages/taler-util/src/types-taler-wallet.ts | 5+++++
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 3+++
Mpackages/taler-wallet-core/src/wallet.ts | 6+++---
4 files changed, 72 insertions(+), 51 deletions(-)

diff --git a/packages/taler-util/src/performance.ts b/packages/taler-util/src/performance.ts @@ -127,15 +127,20 @@ export namespace PerformanceStat { } else if (a.type === PerformanceStatType.Crypto) { return a.operation === b["operation" as keyof typeof b]; } else if (a.type === PerformanceStatType.WalletRequest) { - return a.requestId === b["requestId" as keyof typeof b]; + return false; } else if (a.type === PerformanceStatType.WalletTask) { - return a.taskId === b["taskId" as keyof typeof b]; + return false; } else { assertUnreachable(a); } } } +/** + * Max size of each performance table. + */ +const MAX_PERFORMANCE_TABLE_SIZE = 500; + export type PerformanceTable = { [key in PerformanceStatType]?: PerformanceStat[]; }; @@ -145,59 +150,67 @@ export namespace PerformanceTable { if ("durationMs" in evt && typeof evt.durationMs === "number") { const stat = PerformanceStat.fromNotification(evt); if (!stat) return; - if (!tab[stat.type]) { + insertOrIncrement(tab, stat); + sort(tab); + rotate(tab, stat.type); + } + } + + /** + * Extract the N largest stats of each table. + */ + export function limit(tab: PerformanceTable, n?: number): PerformanceTable { + if (n === undefined || n === Number.MAX_VALUE) { + return tab; + } + const limited: PerformanceTable = {}; + for (const k of Object.keys(tab)) { + const key = k as keyof typeof tab; + limited[key] = tab[key]!!.slice(0, n); + } + return limited; + } + + /** + * Insert event to performance table. + * + * If matching event is found, increment durationMs in place. + */ + function insertOrIncrement(tab: PerformanceTable, stat: PerformanceStat) { + if (!tab[stat.type]) { tab[stat.type] = []; - } + tab[stat.type]?.push(stat); + return; + } + const index = tab[stat.type]!!.findIndex( + (el) => PerformanceStat.equals(el, stat), + ); + if (index === -1) { tab[stat.type]?.push(stat); - - // rotate observable stats to keep each event type at 1000 entries - if (tab[stat.type]!!.length > 1000) { - tab[stat.type]?.splice(0, 1); - } + } else { + const existing = tab[stat.type]!![index]; + existing.durationMs += stat.durationMs; + tab[stat.type]!![index] = existing; } } - export function compactSort(tab: PerformanceTable): PerformanceTable { - const compacted: PerformanceTable = {}; - for (const key of Object.keys(tab)) { - const groupedIdx: number[] = []; - const subtable = tab[key as keyof typeof tab]; - if (!subtable) continue; - - // compare events against each other, - // except the ones already in a group. - for (let a = 0; a < subtable.length; a++) { - if (groupedIdx.includes(a)) continue; - let eventA = subtable[a]; - let totalDuration = eventA.durationMs; - for (let b = 0; b < subtable.length; b++) { - if (b === a) continue; - if (groupedIdx.includes(b)) continue; - let eventB = subtable[b]; - if (PerformanceStat.equals(eventA, eventB)) { - eventA.durationMs += eventB.durationMs; - groupedIdx.push(b); - } - } - - groupedIdx.push(a); - - if (!compacted[key as keyof typeof tab]) { - compacted[key as keyof typeof tab] = []; - } - - // insert in descending order according to durationMs - let insertIndex = compacted[key as keyof typeof tab]!!.findIndex( - (el) => el.durationMs <= eventA.durationMs, - ); - if (insertIndex === -1) { - insertIndex = compacted[key as keyof typeof tab]!!.length; - } - compacted[key as keyof typeof tab]!!.splice(insertIndex, 0, eventA); - } + /** + * Sort all performance tables in place. + */ + function sort(tab: PerformanceTable) { + for (const k of Object.keys(tab)) { + const key = k as keyof typeof tab + tab[key]!!.sort((a, b) => b.durationMs - a.durationMs); } + } - return compacted; + /** + * Keep performance table under size limit. + */ + function rotate(tab: PerformanceTable, type: PerformanceStatType) { + if (tab[type]!!.length > MAX_PERFORMANCE_TABLE_SIZE) { + tab[type]?.splice(0, 1); + } } } diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -4509,6 +4509,11 @@ export interface GetBankingChoicesForPaytoResponse { } export interface GetPerformanceStatsRequest { + /** + * Limit to N largest performance stats of each table. + * + * When undefined, all performance stats will be returned. + */ limit?: number; } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -1507,6 +1507,9 @@ export type GetActiveTasksOp = { response: GetActiveTasksResponse; }; +/** + * Get a list of performance stats for diagnostics. + */ export type GetPerformanceStatsOp = { op: WalletApiOperation.TestingGetPerformanceStats; request: GetPerformanceStatsRequest; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -208,6 +208,7 @@ import { codecForGetExchangeTosRequest, codecForGetMaxDepositAmountRequest, codecForGetMaxPeerPushDebitAmountRequest, + codecForGetPerformanceStatsRequest, codecForGetQrCodesForPaytoRequest, codecForGetTransactionsV2Request, codecForGetWithdrawalDetailsForAmountRequest, @@ -2066,8 +2067,7 @@ export async function handleGetPerformanceStats( req: GetPerformanceStatsRequest, ): Promise<GetPerformanceStatsResponse> { return { - // TODO: implement limit of elements on each table - stats: PerformanceTable.compactSort(wex.ws.performanceStats), + stats: PerformanceTable.limit(wex.ws.performanceStats, req.limit), }; } @@ -2093,7 +2093,7 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { handler: handleConvertIbanPaytoToAccountField, }, [WalletApiOperation.TestingGetPerformanceStats]: { - codec: codecForEmptyObject(), + codec: codecForGetPerformanceStatsRequest(), handler: handleGetPerformanceStats, }, [WalletApiOperation.TestingWaitExchangeState]: {