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:
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]: {