Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions playground/src/components/contract/components/FunctionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ export function FunctionCard({ fn, contract, contractArtifact, onSendTxRequested
return { ...step, subtotal: acc };
});

const totalRPCCalls = Object.values(profileResult.stats.nodeRPCCalls ?? {}).reduce(
(acc, calls) => acc + calls.times.length,
const totalRPCCalls = Object.values(profileResult.stats.nodeRPCCalls?.perMethod ?? {}).reduce(
(acc, calls) => acc + (calls?.times.length ?? 0),
0,
);

Expand Down Expand Up @@ -211,7 +211,7 @@ export function FunctionCard({ fn, contract, contractArtifact, onSendTxRequested
'& .MuiBadge-badge': {
backgroundColor: colors.secondary.main,
color: colors.text.primary,
}
},
}}
></Badge>
</Typography>
Expand Down Expand Up @@ -263,7 +263,15 @@ export function FunctionCard({ fn, contract, contractArtifact, onSendTxRequested
<Typography variant="body1" sx={{ fontWeight: 200, marginRight: '0.5rem' }}>
Simulation results:
</Typography>
<div css={{ backgroundColor: commonStyles.glassDark, border: commonStyles.borderNormal, color: colors.text.primary, padding: '0.5rem', borderRadius: commonStyles.borderRadius }}>
<div
css={{
backgroundColor: commonStyles.glassDark,
border: commonStyles.borderNormal,
color: colors.text.primary,
padding: '0.5rem',
borderRadius: commonStyles.borderRadius,
}}
>
{simulationResults?.success ? (
<Typography variant="body1">{simulationResults?.data ?? 'No return value'}</Typography>
) : (
Expand All @@ -281,7 +289,11 @@ export function FunctionCard({ fn, contract, contractArtifact, onSendTxRequested
<>
<TableContainer
component={Paper}
sx={{ marginRight: '0.5rem', backgroundColor: 'rgba(0, 0, 0, 0.3)', border: '1px solid rgba(212, 255, 40, 0.15)' }}
sx={{
marginRight: '0.5rem',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(212, 255, 40, 0.15)',
}}
>
<Table size="small">
<TableHead>
Expand Down
16 changes: 15 additions & 1 deletion yarn-project/cli-wallet/src/utils/profiling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function printProfileResult(

if (stats.nodeRPCCalls) {
log(format('\nRPC calls:\n'));
for (const [method, { times }] of Object.entries(stats.nodeRPCCalls)) {
for (const [method, { times }] of Object.entries(stats.nodeRPCCalls.perMethod)) {
const calls = times.length;
const total = times.reduce((acc, time) => acc + time, 0);
const avg = total / calls;
Expand All @@ -112,6 +112,20 @@ export function printProfileResult(
),
);
}

const { roundTrips } = stats.nodeRPCCalls;
log(format('\nRound trips (actual blocking waits):\n'));
log(format('Round trips:'.padEnd(25), `${roundTrips.roundTrips}`.padStart(COLUMN_MAX_WIDTH)));
log(
format(
'Total blocking time:'.padEnd(25),
`${roundTrips.totalBlockingTime.toFixed(2)}ms`.padStart(COLUMN_MAX_WIDTH),
),
);
if (roundTrips.roundTrips > 0) {
const avgRoundTrip = roundTrips.totalBlockingTime / roundTrips.roundTrips;
log(format('Avg round trip:'.padEnd(25), `${avgRoundTrip.toFixed(2)}ms`.padStart(COLUMN_MAX_WIDTH)));
}
}

log(format('\nSync time:'.padEnd(25), `${timings.sync?.toFixed(2)}ms`.padStart(16)));
Expand Down
184 changes: 183 additions & 1 deletion yarn-project/end-to-end/src/bench/client_flows/amm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AMMContract } from '@aztec/noir-contracts.js/AMM';
import { FPCContract } from '@aztec/noir-contracts.js/FPC';
import { SponsoredFPCContract } from '@aztec/noir-contracts.js/SponsoredFPC';
import { TokenContract } from '@aztec/noir-contracts.js/Token';
import type { RoundTripStats } from '@aztec/stdlib/tx';
import type { TestWallet } from '@aztec/test-wallet/server';

import { jest } from '@jest/globals';
Expand All @@ -20,7 +21,25 @@ const AMOUNT_PER_NOTE = 1_000_000;

const MINIMUM_NOTES_FOR_RECURSION_LEVEL = [0, 2, 10];

// Set to true to print out the round trip information to the console.
const DEBUG_ROUND_TRIPS = true;

// Expected number of node round trips per account contract and payment method.
const EXPECTED_ROUND_TRIPS: Record<string, number> = {
'ecdsar1+private_fpc': 118,
'ecdsar1+sponsored_fpc': 80,
'schnorr+private_fpc': 118,
'schnorr+sponsored_fpc': 82,
};

interface RoundTripData {
accountType: AccountType;
paymentMethod: BenchmarkingFeePaymentMethod;
roundTrips: RoundTripStats | undefined;
}

describe('AMM benchmark', () => {
const roundTripData: RoundTripData[] = [];
const t = new ClientFlowsBenchmark('amm');
// The wallet used by the admin to interact
let adminWallet: Wallet;
Expand Down Expand Up @@ -70,6 +89,9 @@ describe('AMM benchmark', () => {
});

afterAll(async () => {
if (DEBUG_ROUND_TRIPS) {
printRoundTripDebuggingInfo(roundTripData);
}
await t.teardown();
});

Expand Down Expand Up @@ -158,7 +180,7 @@ describe('AMM benchmark', () => {
.methods.add_liquidity(amountToSend, amountToSend, amountToSend, amountToSend, nonceForAuthwits)
.with({ authWitnesses: [token0Authwit, token1Authwit] });

await captureProfile(
const profileResult = await captureProfile(
`${accountType}+amm_add_liquidity_1_recursions+${benchmarkingPaymentMethod}`,
addLiquidityInteraction,
options,
Expand All @@ -180,6 +202,36 @@ describe('AMM benchmark', () => {
const tx = await addLiquidityInteraction.send({ from: benchysAddress }).wait();
expect(tx.transactionFee!).toBeGreaterThan(0n);
}

if (DEBUG_ROUND_TRIPS) {
roundTripData.push({
accountType,
paymentMethod: benchmarkingPaymentMethod,
roundTrips: profileResult.stats.nodeRPCCalls?.roundTrips,
});
} else {
const roundTripsKey = `${accountType}+${benchmarkingPaymentMethod}`;
const actualRoundTrips = profileResult.stats.nodeRPCCalls?.roundTrips.roundTrips ?? 0;
const expectedRoundTrips = EXPECTED_ROUND_TRIPS[roundTripsKey];
if (expectedRoundTrips === undefined) {
throw new Error(
`Missing expected round trips for ${roundTripsKey}. ` +
`Add '${roundTripsKey}': ${actualRoundTrips} to EXPECTED_ROUND_TRIPS.`,
);
}

// The following check serves as a regression test. If you get a failure and it is expected, just update
// the EXPECTED_ROUND_TRIPS constants. If the failure is unexpected, it needs to be investigated. To
// investigate, set the DEBUG_ROUND_TRIPS environment variable to true on both the `next` branch and
// here, then compare the outputs (look for any newly appearing or missing round trips). To compare the
// outputs I recommend using a diff checker like https://www.diffchecker.com/.
//
// Note that the round trip values depend on this test suite being run in its entirety and in the correct
// order.
//
// If you encounter this failure in CI and are unsure of the cause, contact @benesjan.
expect(actualRoundTrips).toBe(expectedRoundTrips);
}
});
});
}
Expand All @@ -189,4 +241,134 @@ describe('AMM benchmark', () => {
}
});
}

/**
* Prints out the round trip information that can be used to debug unexpected round trip changes.
*/
function printRoundTripDebuggingInfo(data: RoundTripData[]) {
for (const trip of data) {
if (!trip.roundTrips) {
throw new Error(
`Round trip stats are undefined for ${trip.accountType}+${trip.paymentMethod}. This should not happen.`,
);
}
}

const width = 120; // Fixed standard width
const title = ' ROUND TRIP DEBUGGING INFORMATION';

// Helper function to wrap long lines, breaking at commas when possible
function wrapLine(content: string, maxContentWidth: number): string[] {
if (content.length <= maxContentWidth) {
return [content];
}

const lines: string[] = [];
let remaining = content;

while (remaining.length > 0) {
if (remaining.length <= maxContentWidth) {
lines.push(remaining);
break;
}

// Try to find a good break point (comma followed by space)
let breakPoint = maxContentWidth;
const commaIndex = remaining.lastIndexOf(', ', maxContentWidth);
if (commaIndex > maxContentWidth * 0.4) {
// Use comma break if it's not too early (at least 40% into the line)
breakPoint = commaIndex + 2; // Include the comma and space
}

const line = remaining.substring(0, breakPoint);
lines.push(line);
remaining = remaining.substring(breakPoint).trim();
}

return lines;
}

// Group by account type for better organization
const groupedByAccount = data.reduce(
(acc, item) => {
if (!acc[item.accountType]) {
acc[item.accountType] = [];
}
acc[item.accountType].push(item);
return acc;
},
{} as Record<AccountType, RoundTripData[]>,
);

const accountEntries = Object.entries(groupedByAccount) as [AccountType, RoundTripData[]][];
const topBorder = '╔' + '═'.repeat(width - 2) + '╗';
const bottomBorder = '╚' + '═'.repeat(width - 2) + '╝';
const sectionTop = '╠' + '═'.repeat(width - 2) + '╣';
const sectionDivider = '╠' + '─'.repeat(width - 2) + '╣';
const sectionBottom = '╠' + '─'.repeat(width - 2) + '╣';
const leftBorder = '║';
const rightBorder = '║';

let output = '\n' + topBorder + '\n';
output += leftBorder + title.padEnd(width - 2) + rightBorder + '\n';
output += sectionTop + '\n';

for (let accountIdx = 0; accountIdx < accountEntries.length; accountIdx++) {
const [accountType, items] = accountEntries[accountIdx];

if (accountIdx > 0) {
output += sectionDivider + '\n';
}

output += leftBorder + ` Account Type: ${accountType.toUpperCase()}`.padEnd(width - 2) + rightBorder + '\n';
output += sectionDivider + '\n';

for (let itemIdx = 0; itemIdx < items.length; itemIdx++) {
const item = items[itemIdx];

if (!item.roundTrips) {
throw new Error(
`Round trip stats are undefined for ${item.accountType}+${item.paymentMethod}. This should not happen.`,
);
}
const roundTrips = item.roundTrips; // TypeScript now knows it's defined

if (itemIdx > 0) {
output += leftBorder + ' '.repeat(width - 2) + rightBorder + '\n';
}

const key = `${item.accountType}+${item.paymentMethod}`;
output += leftBorder + ` Configuration: ${key}`.padEnd(width - 2) + rightBorder + '\n';
output += leftBorder + ` Total Round Trips: ${roundTrips.roundTrips}`.padEnd(width - 2) + rightBorder + '\n';
output += leftBorder + ` Payment Method: ${item.paymentMethod}`.padEnd(width - 2) + rightBorder + '\n';
output += leftBorder + ' '.repeat(width - 2) + rightBorder + '\n';
output += leftBorder + ' Per Round Trip:'.padEnd(width - 2) + rightBorder + '\n';

for (let i = 0; i < roundTrips.roundTripMethods.length; i++) {
const methods = roundTrips.roundTripMethods[i];
const methodsStr = methods.length > 0 ? methods.join(', ') : '(empty)';
const rtNum = String(i + 1).padStart(3, ' ');
const prefix = ` RT ${rtNum}: `;
const content = `[${methodsStr}]`;

// Calculate available width for content (account for borders and prefix)
// width - 2 for borders, then subtract prefix length
const maxContentWidth = width - 2 - prefix.length;
const wrappedLines = wrapLine(content, maxContentWidth);

for (let lineIdx = 0; lineIdx < wrappedLines.length; lineIdx++) {
const lineContent = lineIdx === 0 ? prefix + wrappedLines[lineIdx] : ' ' + wrappedLines[lineIdx]; // Continuation line with extra indentation
output += leftBorder + lineContent.padEnd(width - 2) + rightBorder + '\n';
}
}
}
}

output += sectionBottom + '\n';
output += leftBorder + ` Total Test Runs: ${data.length}`.padEnd(width - 2) + rightBorder + '\n';
output += bottomBorder + '\n';

// eslint-disable-next-line no-console
console.log(output);
}
});
26 changes: 24 additions & 2 deletions yarn-project/end-to-end/src/bench/client_flows/benchmark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
import type { Logger } from '@aztec/aztec.js/log';
import { createLogger } from '@aztec/foundation/log';
import { type PrivateExecutionStep, serializePrivateExecutionSteps } from '@aztec/stdlib/kernel';
import type { ProvingStats, ProvingTimings, SimulationStats, SimulationTimings } from '@aztec/stdlib/tx';
import type {
ProvingStats,
ProvingTimings,
RoundTripStats,
SimulationStats,
SimulationTimings,
} from '@aztec/stdlib/tx';

import assert from 'node:assert';
import { mkdir, writeFile } from 'node:fs/promises';
Expand Down Expand Up @@ -119,6 +125,7 @@ type ClientFlowBenchmark = {
timings: Omit<ProvingTimings & SimulationTimings, 'perFunction'> & { witgen: number };
maxMemory: number;
rpc: Record<string, CallRecording>;
roundTrips: RoundTripStats;
proverType: ProverType;
minimumTrace: StructuredTrace;
totalGateCount: number;
Expand Down Expand Up @@ -212,6 +219,10 @@ export function generateBenchmark(
}, []);
const timings = stats.timings;
const totalGateCount = steps[steps.length - 1].accGateCount;
const nodeRPCCalls = stats.nodeRPCCalls ?? {
perMethod: {},
roundTrips: { roundTrips: 0, totalBlockingTime: 0, roundTripDurations: [], roundTripMethods: [] },
};
return {
name: flow,
timings: {
Expand All @@ -221,7 +232,7 @@ export function generateBenchmark(
unaccounted: timings.unaccounted,
witgen: timings.perFunction.reduce((acc, fn) => acc + fn.time, 0),
},
rpc: Object.entries(stats.nodeRPCCalls ?? {}).reduce(
rpc: Object.entries(nodeRPCCalls.perMethod).reduce(
(acc, [RPCName, RPCCalls]) => {
const total = RPCCalls.times.reduce((sum, time) => sum + time, 0);
const calls = RPCCalls.times.length;
Expand All @@ -236,6 +247,7 @@ export function generateBenchmark(
},
{} as Record<string, CallRecording>,
),
roundTrips: nodeRPCCalls.roundTrips,
maxMemory,
proverType,
minimumTrace: minimumTrace!,
Expand Down Expand Up @@ -280,6 +292,16 @@ export function convertProfileToGHBenchmark(benchmark: ClientFlowBenchmark): Git
value: totalRPCCalls,
unit: 'calls',
},
{
name: `${benchmark.name}/round_trips`,
value: benchmark.roundTrips.roundTrips,
unit: 'round_trips',
},
{
name: `${benchmark.name}/round_trips_blocking_time`,
value: benchmark.roundTrips.totalBlockingTime,
unit: 'ms',
},
];
if (benchmark.timings.proving) {
benches.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,12 @@ export class ContractFunctionSimulator {
*/
getStats() {
const nodeRPCCalls =
typeof (this.aztecNode as ProxiedNode).getStats === 'function' ? (this.aztecNode as ProxiedNode).getStats() : {};
typeof (this.aztecNode as ProxiedNode).getStats === 'function'
? (this.aztecNode as ProxiedNode).getStats()
: {
perMethod: {},
roundTrips: { roundTrips: 0, totalBlockingTime: 0, roundTripDurations: [], roundTripMethods: [] },
};

return { nodeRPCCalls };
}
Expand Down
Loading
Loading