Skip to content

Commit 3601722

Browse files
authored
chore: update benchmark scraper (#22984)
.
1 parent 4a99638 commit 3601722

2 files changed

Lines changed: 211 additions & 57 deletions

File tree

spartan/scripts/bench_10tps/bench_output.schema.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@
218218
"targetTps": { "type": "number" },
219219
"inclusionTpsMean": {
220220
"type": ["number", "null"],
221-
"description": "Exact block-log inclusion throughput over the observed inclusion window: totalTxsMined / (inclusionEndedAt - startedAt)."
221+
"description": "Inclusion throughput over the observed inclusion window. Uses exact block-log throughput when block records are available, otherwise falls back to the Prometheus inclusionTps mean."
222222
},
223223
"inclusionTpsPeak": {
224224
"type": ["number", "null"],
@@ -231,8 +231,14 @@
231231
"blockBuildDurationP95Ms": { "type": ["number", "null"] },
232232
"publicProcessorTxDurationP50Ms": { "type": ["number", "null"] },
233233
"publicProcessorTxDurationP95Ms": { "type": ["number", "null"] },
234-
"totalTxsMined": { "type": ["integer", "null"] },
235-
"totalTxsFailed": { "type": ["integer", "null"] },
234+
"totalTxsMined": {
235+
"type": ["integer", "null"],
236+
"description": "Exact sum from per-block logs. Null when block logs were unavailable and inclusionTpsMean came from Prometheus."
237+
},
238+
"totalTxsFailed": {
239+
"type": ["integer", "null"],
240+
"description": "Exact sum from per-block logs. Null when block logs were unavailable."
241+
},
236242
"totalSilentSkipCount": {
237243
"type": ["integer", "null"],
238244
"description": "Sum of per-block silentlySkippedCount. > 0 means the post-process blob-field revert path fired during the run."

spartan/scripts/bench_10tps/bench_scrape.ts

Lines changed: 202 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const STEP_SECONDS = 15;
4242
const DRAIN_BUFFER_SECONDS = 90; // OTel batch push 60s + one Prom scrape 15s + slack
4343
const PENDING_POLL_SECONDS = 30;
4444
const DEFAULT_MAX_PENDING_WAIT_SECONDS = 60 * 60;
45+
const GCLOUD_LOG_FRESHNESS = env.BENCH_SCRAPE_GCLOUD_LOG_FRESHNESS ?? "2d";
4546

4647
// --- CLI ---
4748

@@ -485,7 +486,7 @@ async function gcloudRead(filter: string): Promise<GcloudEntry[]> {
485486
GCP_PROJECT_ID,
486487
"--format=json",
487488
"--order=asc",
488-
"--freshness=24h",
489+
`--freshness=${GCLOUD_LOG_FRESHNESS}`,
489490
"--limit=50000",
490491
],
491492
{ stdio: ["ignore", "pipe", "pipe"] },
@@ -755,67 +756,203 @@ async function scrapeBlocks(
755756
startedAt: string,
756757
endedAt: string,
757758
): Promise<BlockRecord[]> {
758-
const filter = [
759+
const canonicalFilter = [
760+
`resource.labels.namespace_name="${NAMESPACE}"`,
761+
`resource.labels.pod_name=~"${NAMESPACE}-(validator|rpc).*"`,
762+
`jsonPayload.eventName="l2-block-handled"`,
763+
timeFilter(startedAt, endedAt),
764+
].join(" AND ");
765+
const builtFilter = [
766+
`resource.labels.namespace_name="${NAMESPACE}"`,
767+
`resource.labels.pod_name=~"${NAMESPACE}-(validator|rpc).*"`,
768+
`jsonPayload.eventName="l2-block-built"`,
769+
timeFilter(startedAt, endedAt),
770+
].join(" AND ");
771+
const processorFilter = [
759772
`resource.labels.namespace_name="${NAMESPACE}"`,
760773
`resource.labels.pod_name=~"${NAMESPACE}-(validator|rpc).*"`,
761774
`jsonPayload.message=~"^Processed [0-9]+ successful txs and"`,
762775
timeFilter(startedAt, endedAt),
763776
].join(" AND ");
764-
const entries = await gcloudRead(filter);
765777

766-
// Each block is logged once per pod that processed it (validators +
767-
// RPC-colocated full node sync). Dedupe by blockNumber, keep the earliest
768-
// timestamp — that's most likely the proposer who built the block.
769-
const byBlock = new Map<number, { entry: GcloudEntry; time: number }>();
770-
for (const entry of entries) {
771-
const p = entry.jsonPayload;
772-
if (!p) {
773-
continue;
774-
}
775-
const blockNumber =
776-
typeof p.blockNumber === "number"
777-
? p.blockNumber
778-
: typeof p.blockNumber === "string"
779-
? Number(p.blockNumber)
780-
: NaN;
781-
if (!Number.isFinite(blockNumber)) {
782-
continue;
783-
}
784-
const t = Date.parse(entry.timestamp);
785-
const prev = byBlock.get(blockNumber);
786-
if (!prev || t < prev.time) {
787-
byBlock.set(blockNumber, { entry, time: t });
788-
}
789-
}
778+
const [canonicalEntries, builtEntries, processorEntries] = await Promise.all([
779+
gcloudRead(canonicalFilter),
780+
gcloudRead(builtFilter).catch((err) => {
781+
log("built-block log scrape failed, continuing without build durations", {
782+
err: err instanceof Error ? err.message : String(err),
783+
});
784+
return [] as GcloudEntry[];
785+
}),
786+
gcloudRead(processorFilter).catch((err) => {
787+
log(
788+
"public-processor log scrape failed, continuing without gas/silent-skip fields",
789+
{
790+
err: err instanceof Error ? err.message : String(err),
791+
},
792+
);
793+
return [] as GcloudEntry[];
794+
}),
795+
]);
790796

791-
if (byBlock.size === 0) {
797+
const canonicalByBlock = dedupeBlockEntries(canonicalEntries, (entry) => ({
798+
blockNumber: numberPayloadField(entry.jsonPayload ?? {}, "blockNumber"),
799+
txCount: numberPayloadField(entry.jsonPayload ?? {}, "txCount"),
800+
time: Date.parse(entry.timestamp),
801+
}));
802+
const builtByBlock = entriesByBlock(builtEntries);
803+
const processorByBlock = entriesByBlock(processorEntries);
804+
805+
if (canonicalByBlock.size === 0) {
792806
return [];
793807
}
794-
const blockNumbers = [...byBlock.keys()].sort((a, b) => a - b);
808+
const blockNumbers = [...canonicalByBlock.keys()].sort((a, b) => a - b);
795809
const first = blockNumbers[0];
796810

797811
return blockNumbers.map((bn) => {
798-
const { entry } = byBlock.get(bn)!;
799-
const p = entry.jsonPayload!;
812+
const canonical = canonicalByBlock.get(bn)!;
813+
const p = canonical.jsonPayload!;
814+
const txCount = finiteOrZero(numberPayloadField(p, "txCount"));
815+
const built = chooseBestMatchingEntry(
816+
builtByBlock.get(bn) ?? [],
817+
txCount,
818+
"txCount",
819+
);
820+
const processor = chooseBestMatchingEntry(
821+
processorByBlock.get(bn) ?? [],
822+
txCount,
823+
"successfulCount",
824+
);
825+
const processorPayload = processor?.jsonPayload;
800826
return {
801827
blockNumber: bn,
802828
blockNumberInTest: bn - first,
803-
minedAt: entry.timestamp,
804-
successfulCount: Number(p.successfulCount ?? 0),
805-
failedCount: Number(p.failedCount ?? 0),
806-
silentlySkippedCount: Number(p.silentlySkippedCount ?? 0),
807-
silentlySkippedDurationMs: Number(p.silentlySkippedDurationMs ?? 0),
808-
buildDurationSeconds: Number(p.duration ?? 0),
809-
totalPublicGas: p.totalPublicGas as
829+
minedAt: canonical.timestamp,
830+
successfulCount: txCount,
831+
failedCount: finiteOrZero(
832+
numberPayloadField(processorPayload ?? {}, "failedCount"),
833+
),
834+
silentlySkippedCount: finiteOrZero(
835+
numberPayloadField(processorPayload ?? {}, "silentlySkippedCount"),
836+
),
837+
silentlySkippedDurationMs: finiteOrZero(
838+
numberPayloadField(processorPayload ?? {}, "silentlySkippedDurationMs"),
839+
),
840+
buildDurationSeconds:
841+
built?.jsonPayload === undefined
842+
? finiteOrZero(numberPayloadField(processorPayload ?? {}, "duration"))
843+
: finiteOrZero(numberPayloadField(built.jsonPayload, "duration")) /
844+
1000,
845+
totalPublicGas: processorPayload?.totalPublicGas as
810846
| { daGas: number; l2Gas: number }
811847
| undefined,
812848
totalSizeInBytes:
813-
typeof p.totalSizeInBytes === "number" ? p.totalSizeInBytes : undefined,
849+
typeof processorPayload?.totalSizeInBytes === "number"
850+
? processorPayload.totalSizeInBytes
851+
: undefined,
814852
source: "log",
815853
};
816854
});
817855
}
818856

857+
type BlockEntryProjection = {
858+
blockNumber: number;
859+
txCount: number;
860+
time: number;
861+
};
862+
863+
function dedupeBlockEntries(
864+
entries: GcloudEntry[],
865+
project: (entry: GcloudEntry) => BlockEntryProjection,
866+
): Map<number, GcloudEntry> {
867+
const byBlock = new Map<
868+
number,
869+
{ entry: GcloudEntry; projection: BlockEntryProjection }
870+
>();
871+
for (const entry of entries) {
872+
const projection = project(entry);
873+
if (
874+
!Number.isFinite(projection.blockNumber) ||
875+
!Number.isFinite(projection.txCount) ||
876+
!Number.isFinite(projection.time)
877+
) {
878+
continue;
879+
}
880+
const prev = byBlock.get(projection.blockNumber);
881+
if (!prev || isBetterCanonicalBlockEntry(projection, prev.projection)) {
882+
byBlock.set(projection.blockNumber, { entry, projection });
883+
}
884+
}
885+
return new Map(
886+
[...byBlock.entries()].map(([blockNumber, value]) => [
887+
blockNumber,
888+
value.entry,
889+
]),
890+
);
891+
}
892+
893+
function isBetterCanonicalBlockEntry(
894+
candidate: BlockEntryProjection,
895+
previous: BlockEntryProjection,
896+
): boolean {
897+
// Same tx count usually means the same block observed by another pod; keep
898+
// the earliest timestamp. Different tx count implies a distinct block at the
899+
// same height, so prefer the later observation as the best final-chain proxy.
900+
if (candidate.txCount !== previous.txCount) {
901+
return candidate.time > previous.time;
902+
}
903+
return candidate.time < previous.time;
904+
}
905+
906+
function entriesByBlock(entries: GcloudEntry[]): Map<number, GcloudEntry[]> {
907+
const out = new Map<number, GcloudEntry[]>();
908+
for (const entry of entries) {
909+
const blockNumber = numberPayloadField(
910+
entry.jsonPayload ?? {},
911+
"blockNumber",
912+
);
913+
if (!Number.isFinite(blockNumber)) {
914+
continue;
915+
}
916+
const bucket = out.get(blockNumber) ?? [];
917+
bucket.push(entry);
918+
out.set(blockNumber, bucket);
919+
}
920+
return out;
921+
}
922+
923+
function chooseBestMatchingEntry(
924+
entries: GcloudEntry[],
925+
txCount: number,
926+
txCountField: string,
927+
): GcloudEntry | undefined {
928+
const candidates = entries.filter(
929+
(entry) =>
930+
numberPayloadField(entry.jsonPayload ?? {}, txCountField) === txCount,
931+
);
932+
const source = candidates.length > 0 ? candidates : entries;
933+
return source
934+
.filter((entry) => Number.isFinite(Date.parse(entry.timestamp)))
935+
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))[0];
936+
}
937+
938+
function numberPayloadField(
939+
payload: Record<string, unknown>,
940+
key: string,
941+
): number {
942+
const value = payload[key];
943+
if (typeof value === "number") {
944+
return value;
945+
}
946+
if (typeof value === "string") {
947+
return Number(value);
948+
}
949+
return NaN;
950+
}
951+
952+
function finiteOrZero(value: number): number {
953+
return Number.isFinite(value) ? value : 0;
954+
}
955+
819956
type ChainPrunedEvent = {
820957
at: string;
821958
type: "chainPruned";
@@ -1289,16 +1426,27 @@ async function buildSummary(a: SummaryArgs): Promise<Record<string, unknown>> {
12891426
minedAtEpoch <= a.inclusionEndedAtEpoch
12901427
);
12911428
});
1292-
const totalTxsMined = inclusionBlocks.reduce(
1293-
(s, b) => s + b.successfulCount,
1294-
0,
1295-
);
1429+
const hasInclusionBlockRecords = inclusionBlocks.length > 0;
1430+
const totalTxsMined = hasInclusionBlockRecords
1431+
? inclusionBlocks.reduce((s, b) => s + b.successfulCount, 0)
1432+
: null;
1433+
const promInclusionTpsMean = meanNonNull(inclusionPoints);
12961434
const inclusionTpsMean =
1297-
a.windowSec > 0
1435+
totalTxsMined !== null && a.windowSec > 0
12981436
? totalTxsMined / a.windowSec
1299-
: meanNonNull(inclusionPoints);
1437+
: promInclusionTpsMean;
13001438
const inclusionTpsPeak = maxNonNull(inclusionPoints);
13011439

1440+
if (!hasInclusionBlockRecords && promInclusionTpsMean !== null) {
1441+
log(
1442+
"No block records found in inclusion window; using Prometheus inclusion TPS mean for summary",
1443+
{
1444+
promInclusionTpsMean,
1445+
inclusionPointCount: inclusionPoints.length,
1446+
},
1447+
);
1448+
}
1449+
13021450
const safeInstant = async (promql: string): Promise<number | null> => {
13031451
try {
13041452
return await queryInstant(promql, a.endedAtEpoch);
@@ -1379,15 +1527,15 @@ async function buildSummary(a: SummaryArgs): Promise<Record<string, unknown>> {
13791527
publicProcessorTxDurationP50Ms: ppTxP50,
13801528
publicProcessorTxDurationP95Ms: ppTxP95,
13811529
totalTxsMined,
1382-
totalTxsFailed: inclusionBlocks.reduce((s, b) => s + b.failedCount, 0),
1383-
totalSilentSkipCount: inclusionBlocks.reduce(
1384-
(s, b) => s + b.silentlySkippedCount,
1385-
0,
1386-
),
1387-
totalSilentSkipDurationMs: inclusionBlocks.reduce(
1388-
(s, b) => s + b.silentlySkippedDurationMs,
1389-
0,
1390-
),
1530+
totalTxsFailed: hasInclusionBlockRecords
1531+
? inclusionBlocks.reduce((s, b) => s + b.failedCount, 0)
1532+
: null,
1533+
totalSilentSkipCount: hasInclusionBlockRecords
1534+
? inclusionBlocks.reduce((s, b) => s + b.silentlySkippedCount, 0)
1535+
: null,
1536+
totalSilentSkipDurationMs: hasInclusionBlockRecords
1537+
? inclusionBlocks.reduce((s, b) => s + b.silentlySkippedDurationMs, 0)
1538+
: null,
13911539
reorgCount: reorgs.length,
13921540
deepestReorgBlocks: deepest,
13931541
};

0 commit comments

Comments
 (0)