@@ -42,6 +42,7 @@ const STEP_SECONDS = 15;
4242const DRAIN_BUFFER_SECONDS = 90 ; // OTel batch push 60s + one Prom scrape 15s + slack
4343const PENDING_POLL_SECONDS = 30 ;
4444const 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+
819956type 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