Skip to content

Commit 5b76b88

Browse files
committed
feat: measuring node round trips
1 parent 59e53a5 commit 5b76b88

File tree

5 files changed

+126
-20
lines changed

5 files changed

+126
-20
lines changed

yarn-project/cli-wallet/src/utils/profiling.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function printProfileResult(
9595

9696
if (stats.nodeRPCCalls) {
9797
log(format('\nRPC calls:\n'));
98-
for (const [method, { times }] of Object.entries(stats.nodeRPCCalls)) {
98+
for (const [method, { times }] of Object.entries(stats.nodeRPCCalls.perMethod)) {
9999
const calls = times.length;
100100
const total = times.reduce((acc, time) => acc + time, 0);
101101
const avg = total / calls;
@@ -112,6 +112,20 @@ export function printProfileResult(
112112
),
113113
);
114114
}
115+
116+
const { roundTrips } = stats.nodeRPCCalls;
117+
log(format('\nRound trips (actual blocking waits):\n'));
118+
log(format('Round trips:'.padEnd(25), `${roundTrips.roundTrips}`.padStart(COLUMN_MAX_WIDTH)));
119+
log(
120+
format(
121+
'Total blocking time:'.padEnd(25),
122+
`${roundTrips.totalBlockingTime.toFixed(2)}ms`.padStart(COLUMN_MAX_WIDTH),
123+
),
124+
);
125+
if (roundTrips.roundTrips > 0) {
126+
const avgRoundTrip = roundTrips.totalBlockingTime / roundTrips.roundTrips;
127+
log(format('Avg round trip:'.padEnd(25), `${avgRoundTrip.toFixed(2)}ms`.padStart(COLUMN_MAX_WIDTH)));
128+
}
115129
}
116130

117131
log(format('\nSync time:'.padEnd(25), `${timings.sync?.toFixed(2)}ms`.padStart(16)));

yarn-project/end-to-end/src/bench/client_flows/benchmark.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ import {
77
import type { Logger } from '@aztec/aztec.js/log';
88
import { createLogger } from '@aztec/foundation/log';
99
import { type PrivateExecutionStep, serializePrivateExecutionSteps } from '@aztec/stdlib/kernel';
10-
import type { ProvingStats, ProvingTimings, SimulationStats, SimulationTimings } from '@aztec/stdlib/tx';
10+
import type {
11+
ProvingStats,
12+
ProvingTimings,
13+
RoundTripStats,
14+
SimulationStats,
15+
SimulationTimings,
16+
} from '@aztec/stdlib/tx';
1117

1218
import assert from 'node:assert';
1319
import { mkdir, writeFile } from 'node:fs/promises';
@@ -119,6 +125,7 @@ type ClientFlowBenchmark = {
119125
timings: Omit<ProvingTimings & SimulationTimings, 'perFunction'> & { witgen: number };
120126
maxMemory: number;
121127
rpc: Record<string, CallRecording>;
128+
roundTrips: RoundTripStats;
122129
proverType: ProverType;
123130
minimumTrace: StructuredTrace;
124131
totalGateCount: number;
@@ -212,6 +219,10 @@ export function generateBenchmark(
212219
}, []);
213220
const timings = stats.timings;
214221
const totalGateCount = steps[steps.length - 1].accGateCount;
222+
const nodeRPCCalls = stats.nodeRPCCalls ?? {
223+
perMethod: {},
224+
roundTrips: { roundTrips: 0, totalBlockingTime: 0, roundTripDurations: [] },
225+
};
215226
return {
216227
name: flow,
217228
timings: {
@@ -221,7 +232,7 @@ export function generateBenchmark(
221232
unaccounted: timings.unaccounted,
222233
witgen: timings.perFunction.reduce((acc, fn) => acc + fn.time, 0),
223234
},
224-
rpc: Object.entries(stats.nodeRPCCalls ?? {}).reduce(
235+
rpc: Object.entries(nodeRPCCalls.perMethod).reduce(
225236
(acc, [RPCName, RPCCalls]) => {
226237
const total = RPCCalls.times.reduce((sum, time) => sum + time, 0);
227238
const calls = RPCCalls.times.length;
@@ -236,6 +247,7 @@ export function generateBenchmark(
236247
},
237248
{} as Record<string, CallRecording>,
238249
),
250+
roundTrips: nodeRPCCalls.roundTrips,
239251
maxMemory,
240252
proverType,
241253
minimumTrace: minimumTrace!,
@@ -280,6 +292,16 @@ export function convertProfileToGHBenchmark(benchmark: ClientFlowBenchmark): Git
280292
value: totalRPCCalls,
281293
unit: 'calls',
282294
},
295+
{
296+
name: `${benchmark.name}/round_trips`,
297+
value: benchmark.roundTrips.roundTrips,
298+
unit: 'round_trips',
299+
},
300+
{
301+
name: `${benchmark.name}/round_trips_blocking_time`,
302+
value: benchmark.roundTrips.totalBlockingTime,
303+
unit: 'ms',
304+
},
283305
];
284306
if (benchmark.timings.proving) {
285307
benches.push({

yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,9 @@ export class ContractFunctionSimulator {
331331
*/
332332
getStats() {
333333
const nodeRPCCalls =
334-
typeof (this.aztecNode as ProxiedNode).getStats === 'function' ? (this.aztecNode as ProxiedNode).getStats() : {};
334+
typeof (this.aztecNode as ProxiedNode).getStats === 'function'
335+
? (this.aztecNode as ProxiedNode).getStats()
336+
: { perMethod: {}, roundTrips: { roundTrips: 0, totalBlockingTime: 0, roundTripDurations: [] } };
335337

336338
return { nodeRPCCalls };
337339
}

yarn-project/pxe/src/contract_function_simulator/proxied_node.ts

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,69 @@
11
import { Timer } from '@aztec/foundation/timer';
22
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
3-
import type { NodeStats } from '@aztec/stdlib/tx';
3+
import type { NodeStats, RoundTripStats } from '@aztec/stdlib/tx';
44

55
/*
6-
* Proxy generator for an AztecNode that tracks the time taken for each RPC call.
6+
* Proxy generator for an AztecNode that tracks the time taken for each RPC call
7+
* and the number of round trips (actual blocking waits for node responses).
8+
*
9+
* A round trip is counted when we transition from 0 to 1 in-flight calls,
10+
* and ends when all concurrent calls complete. This means parallel calls
11+
* in Promise.all count as a single round trip.
712
*/
813
export type ProxiedNode = AztecNode & { getStats(): NodeStats };
914

1015
export class ProxiedNodeFactory {
11-
static create(node: AztecNode) {
12-
const stats: Partial<Record<keyof AztecNode, { times: number[] }>> = {};
16+
static create(node: AztecNode): ProxiedNode {
17+
// Per-method call stats
18+
const perMethod: Partial<Record<keyof AztecNode, { times: number[] }>> = {};
19+
20+
// Round trip tracking
21+
let inFlightCount = 0;
22+
let currentRoundTripTimer: Timer | null = null;
23+
const roundTrips: RoundTripStats = {
24+
roundTrips: 0,
25+
totalBlockingTime: 0,
26+
roundTripDurations: [],
27+
};
28+
1329
return new Proxy(node, {
1430
get(target, prop: keyof ProxiedNode) {
1531
if (prop === 'getStats') {
16-
return () => {
17-
return stats;
32+
return (): NodeStats => {
33+
return { perMethod, roundTrips };
1834
};
1935
} else {
2036
return async function (...args: any[]) {
21-
if (!stats[prop]) {
22-
stats[prop] = { times: [] };
37+
// Track per-method stats
38+
if (!perMethod[prop]) {
39+
perMethod[prop] = { times: [] };
40+
}
41+
42+
// Start of a new round trip batch?
43+
if (inFlightCount === 0) {
44+
roundTrips.roundTrips++;
45+
currentRoundTripTimer = new Timer();
46+
}
47+
inFlightCount++;
48+
49+
const callTimer = new Timer();
50+
try {
51+
const result = await (target[prop] as any).apply(target, args);
52+
return result;
53+
} finally {
54+
const callTime = callTimer.ms();
55+
perMethod[prop].times.push(callTime);
56+
57+
inFlightCount--;
58+
59+
// End of round trip batch - all concurrent calls completed
60+
if (inFlightCount === 0 && currentRoundTripTimer) {
61+
const roundTripTime = currentRoundTripTimer.ms();
62+
roundTrips.totalBlockingTime += roundTripTime;
63+
roundTrips.roundTripDurations.push(roundTripTime);
64+
currentRoundTripTimer = null;
65+
}
2366
}
24-
const timer = new Timer();
25-
const result = await (target[prop] as any).apply(target, args);
26-
stats[prop].times.push(timer.ms());
27-
return result;
2867
};
2968
}
3069
},

yarn-project/stdlib/src/tx/profiling.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,32 @@ import { z } from 'zod';
66
import type { AztecNode } from '../interfaces/aztec-node.js';
77
import { type PrivateExecutionStep, PrivateExecutionStepSchema } from '../kernel/private_kernel_prover_output.js';
88

9-
export type NodeStats = Partial<Record<keyof AztecNode, { times: number[] }>>;
9+
export type RoundTripStats = {
10+
/** Number of round trips (times we blocked waiting for node responses) */
11+
roundTrips: number;
12+
/** Total wall-clock time spent waiting on node (excludes parallel overlap) */
13+
totalBlockingTime: number;
14+
/** Individual round trip durations */
15+
roundTripDurations: number[];
16+
};
17+
18+
const RoundTripStatsSchema = z.object({
19+
roundTrips: z.number(),
20+
totalBlockingTime: z.number(),
21+
roundTripDurations: z.array(z.number()),
22+
});
23+
24+
export type NodeStats = {
25+
/** Per-method call stats */
26+
perMethod: Partial<Record<keyof AztecNode, { times: number[] }>>;
27+
/** Round trip stats tracking actual blocking waits */
28+
roundTrips: RoundTripStats;
29+
};
1030

11-
const NodeStatsSchema = z.record(z.string(), z.object({ times: z.array(z.number()) }));
31+
const NodeStatsSchema = z.object({
32+
perMethod: z.record(z.string(), z.object({ times: z.array(z.number()) })),
33+
roundTrips: RoundTripStatsSchema,
34+
});
1235

1336
type FunctionTiming = {
1437
functionName: string;
@@ -105,7 +128,10 @@ export class TxProfileResult {
105128
},
106129
],
107130
{
108-
nodeRPCCalls: { getBlockHeader: { times: [1] } },
131+
nodeRPCCalls: {
132+
perMethod: { getBlockHeader: { times: [1] } },
133+
roundTrips: { roundTrips: 1, totalBlockingTime: 1, roundTripDurations: [1] },
134+
},
109135
timings: {
110136
sync: 1,
111137
proving: 1,
@@ -140,7 +166,10 @@ export class UtilitySimulationResult {
140166

141167
static random(): UtilitySimulationResult {
142168
return new UtilitySimulationResult([Fr.random()], {
143-
nodeRPCCalls: { getBlockHeader: { times: [1] } },
169+
nodeRPCCalls: {
170+
perMethod: { getBlockHeader: { times: [1] } },
171+
roundTrips: { roundTrips: 1, totalBlockingTime: 1, roundTripDurations: [1] },
172+
},
144173
timings: {
145174
sync: 1,
146175
publicSimulation: 1,

0 commit comments

Comments
 (0)