Add block notification timing logs

This commit is contained in:
Ben
2026-07-12 00:31:12 -04:00
parent bc6f2eebd8
commit 3d58802feb
4 changed files with 56 additions and 7 deletions
+2
View File
@@ -54,6 +54,8 @@ export interface IBlockTemplate {
payoutMode?: PayoutMode | 'all'; payoutMode?: PayoutMode | 'all';
/** Correlates master detection, Redis delivery, and socket fan-out traces. */ /** Correlates master detection, Redis delivery, and socket fan-out traces. */
notificationEventId?: string; notificationEventId?: string;
/** Wall-clock time when the master first observed the source block notification. */
sourceNotificationReceivedAtMs?: number;
notificationPublishedAtMs?: number; notificationPublishedAtMs?: number;
/** Timestamp of the rolling PPLNS snapshot seed used by an empty bridge. */ /** Timestamp of the rolling PPLNS snapshot seed used by an empty bridge. */
payoutBridgeSeedCreatedAtMs?: number; payoutBridgeSeedCreatedAtMs?: number;
+47 -6
View File
@@ -23,6 +23,7 @@ interface BlockNotificationTrace {
reason: TemplateRefreshReason; reason: TemplateRefreshReason;
startedWallMs: number; startedWallMs: number;
startedMonotonic: bigint; startedMonotonic: bigint;
sourceNotificationReceivedAtMs?: number;
stages: Record<string, number>; stages: Record<string, number>;
} }
@@ -235,10 +236,11 @@ export class BitcoinRpcService implements OnModuleInit {
private async listenForNewBlocks(sock: zmq.Subscriber) { private async listenForNewBlocks(sock: zmq.Subscriber) {
for await (const [topic, msg] of sock) { for await (const [topic, msg] of sock) {
const sourceNotificationReceivedAtMs = Date.now();
console.log("New Block"); console.log("New Block");
const miningInfoRefresh = this.getMiningInfo(); const miningInfoRefresh = this.getMiningInfo();
try { try {
await this.getAndBroadcastLatestTemplate('new_block'); await this.getAndBroadcastLatestTemplate('new_block', undefined, sourceNotificationReceivedAtMs);
} catch (error) { } catch (error) {
console.error(`ZMQ block template refresh failed: ${error.message}`); console.error(`ZMQ block template refresh failed: ${error.message}`);
continue; continue;
@@ -264,32 +266,34 @@ export class BitcoinRpcService implements OnModuleInit {
public async getAndBroadcastLatestTemplate( public async getAndBroadcastLatestTemplate(
reason: TemplateRefreshReason = 'periodic', reason: TemplateRefreshReason = 'periodic',
longpollId?: string, longpollId?: string,
sourceNotificationReceivedAtMs?: number,
) { ) {
if (reason === 'periodic') { if (reason === 'periodic') {
if (this.periodicTemplateRefresh != null) { if (this.periodicTemplateRefresh != null) {
await this.periodicTemplateRefresh; await this.periodicTemplateRefresh;
return; return;
} }
const refresh = this.getAndBroadcastLatestTemplateOnce(reason, longpollId); const refresh = this.getAndBroadcastLatestTemplateOnce(reason, longpollId, sourceNotificationReceivedAtMs);
this.periodicTemplateRefresh = refresh.finally(() => { this.periodicTemplateRefresh = refresh.finally(() => {
this.periodicTemplateRefresh = null; this.periodicTemplateRefresh = null;
}); });
await this.periodicTemplateRefresh; await this.periodicTemplateRefresh;
return; return;
} }
await this.getAndBroadcastLatestTemplateOnce(reason, longpollId); await this.getAndBroadcastLatestTemplateOnce(reason, longpollId, sourceNotificationReceivedAtMs);
} }
private async getAndBroadcastLatestTemplateOnce( private async getAndBroadcastLatestTemplateOnce(
reason: TemplateRefreshReason, reason: TemplateRefreshReason,
longpollId?: string, longpollId?: string,
sourceNotificationReceivedAtMs?: number,
): Promise<void> { ): Promise<void> {
if (this.miningInfo?.blocks == null) { if (this.miningInfo?.blocks == null) {
console.warn('Skipping block template broadcast because mining info is not available'); console.warn('Skipping block template broadcast because mining info is not available');
return; return;
} }
const trace = this.startTrace(reason); const trace = this.startTrace(reason, sourceNotificationReceivedAtMs);
const blockTemplate = await this.fetchBlockTemplate(trace, longpollId); const blockTemplate = await this.fetchBlockTemplate(trace, longpollId);
if (blockTemplate == null) { if (blockTemplate == null) {
console.warn(`Skipping block template broadcast for height ${this.miningInfo.blocks}; block template is not available`); console.warn(`Skipping block template broadcast for height ${this.miningInfo.blocks}; block template is not available`);
@@ -339,6 +343,9 @@ export class BitcoinRpcService implements OnModuleInit {
const isNewTip = tipKey !== this.lastPublishedTipKey; const isNewTip = tipKey !== this.lastPublishedTipKey;
this.miningInfo = { ...this.miningInfo, blocks: tipHeight }; this.miningInfo = { ...this.miningInfo, blocks: tipHeight };
this.markTrace(trace, 'template_ready'); this.markTrace(trace, 'template_ready');
if (isNewTip) {
this.logSourceNotification(trace, blockTemplate);
}
if (isNewTip) { if (isNewTip) {
// Start bridge publication first, but never await it on the // Start bridge publication first, but never await it on the
@@ -362,6 +369,7 @@ export class BitcoinRpcService implements OnModuleInit {
forceCleanJobs: isNewTip, forceCleanJobs: isNewTip,
jobType: 'full', jobType: 'full',
notificationEventId: trace.eventId, notificationEventId: trace.eventId,
sourceNotificationReceivedAtMs: trace.sourceNotificationReceivedAtMs,
notificationPublishedAtMs: Date.now(), notificationPublishedAtMs: Date.now(),
}; };
@@ -630,7 +638,9 @@ export class BitcoinRpcService implements OnModuleInit {
} }
if (trace.reason === 'longpoll') { if (trace.reason === 'longpoll') {
const waitMs = Number(process.hrtime.bigint() - trace.startedMonotonic) / 1e6; const waitMs = Number(process.hrtime.bigint() - trace.startedMonotonic) / 1e6;
trace.startedWallMs = Date.now(); const sourceNotificationReceivedAtMs = Date.now();
trace.sourceNotificationReceivedAtMs = sourceNotificationReceivedAtMs;
trace.startedWallMs = sourceNotificationReceivedAtMs;
trace.startedMonotonic = process.hrtime.bigint(); trace.startedMonotonic = process.hrtime.bigint();
trace.stages = { start: 0, longpollWait: waitMs }; trace.stages = { start: 0, longpollWait: waitMs };
} else { } else {
@@ -758,6 +768,7 @@ export class BitcoinRpcService implements OnModuleInit {
forceCleanJobs, forceCleanJobs,
jobType: 'full', jobType: 'full',
notificationEventId: `${trace.eventId}:pplns`, notificationEventId: `${trace.eventId}:pplns`,
sourceNotificationReceivedAtMs: trace.sourceNotificationReceivedAtMs,
notificationPublishedAtMs: Date.now(), notificationPublishedAtMs: Date.now(),
}; };
legacyTemplate = pplnsTemplate; legacyTemplate = pplnsTemplate;
@@ -842,6 +853,7 @@ export class BitcoinRpcService implements OnModuleInit {
Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000),
); );
bridgeTemplate.notificationEventId = `${trace.eventId}:bridge`; bridgeTemplate.notificationEventId = `${trace.eventId}:bridge`;
bridgeTemplate.sourceNotificationReceivedAtMs = trace.sourceNotificationReceivedAtMs;
const update: Sv1BridgeUpdate = { const update: Sv1BridgeUpdate = {
schemaVersion: 1, schemaVersion: 1,
type: 'subsidy-bridge', type: 'subsidy-bridge',
@@ -919,6 +931,7 @@ export class BitcoinRpcService implements OnModuleInit {
Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000),
); );
bridgeTemplate.notificationEventId = `${trace.eventId}:bridge:pplns`; bridgeTemplate.notificationEventId = `${trace.eventId}:bridge:pplns`;
bridgeTemplate.sourceNotificationReceivedAtMs = trace.sourceNotificationReceivedAtMs;
bridgeTemplate.payoutBridgeSeedCreatedAtMs = seed.preparedAtMs; bridgeTemplate.payoutBridgeSeedCreatedAtMs = seed.preparedAtMs;
const update: Sv1BridgeUpdate = { const update: Sv1BridgeUpdate = {
schemaVersion: 1, schemaVersion: 1,
@@ -1427,12 +1440,16 @@ export class BitcoinRpcService implements OnModuleInit {
return rpcUrl.toString(); return rpcUrl.toString();
} }
private startTrace(reason: TemplateRefreshReason): BlockNotificationTrace { private startTrace(
reason: TemplateRefreshReason,
sourceNotificationReceivedAtMs?: number,
): BlockNotificationTrace {
return { return {
eventId: `${reason}:${Date.now()}:${this.rpcRequestId + 1}`, eventId: `${reason}:${Date.now()}:${this.rpcRequestId + 1}`,
reason, reason,
startedWallMs: Date.now(), startedWallMs: Date.now(),
startedMonotonic: process.hrtime.bigint(), startedMonotonic: process.hrtime.bigint(),
sourceNotificationReceivedAtMs,
stages: { start: 0 }, stages: { start: 0 },
}; };
} }
@@ -1441,6 +1458,26 @@ export class BitcoinRpcService implements OnModuleInit {
trace.stages[stage] = Number(process.hrtime.bigint() - trace.startedMonotonic) / 1e6; trace.stages[stage] = Number(process.hrtime.bigint() - trace.startedMonotonic) / 1e6;
} }
private logSourceNotification(trace: BlockNotificationTrace, blockTemplate: IBlockTemplate): void {
if (trace.sourceNotificationReceivedAtMs == null
|| (trace.reason !== 'new_block' && trace.reason !== 'longpoll')) {
return;
}
console.log(JSON.stringify({
event: 'block_source_notification',
eventId: trace.eventId,
source: trace.reason === 'new_block' ? 'zmq' : 'longpoll',
receivedAt: new Date(trace.sourceNotificationReceivedAtMs).toISOString(),
receivedAtMs: trace.sourceNotificationReceivedAtMs,
tipHeight: blockTemplate.height - 1,
templateHeight: blockTemplate.height,
previousBlockHash: blockTemplate.previousblockhash,
sourceToTemplateReadyMs: trace.stages.template_ready,
stagesMs: trace.stages,
}));
}
private logTrace(trace: BlockNotificationTrace, blockTemplate: IBlockTemplate): void { private logTrace(trace: BlockNotificationTrace, blockTemplate: IBlockTemplate): void {
if (!this.shouldLogBlockNotificationTrace(trace)) { if (!this.shouldLogBlockNotificationTrace(trace)) {
return; return;
@@ -1456,6 +1493,10 @@ export class BitcoinRpcService implements OnModuleInit {
payoutMode: blockTemplate.payoutMode ?? 'all', payoutMode: blockTemplate.payoutMode ?? 'all',
jobType: blockTemplate.jobType ?? 'full', jobType: blockTemplate.jobType ?? 'full',
startedAt: new Date(trace.startedWallMs).toISOString(), startedAt: new Date(trace.startedWallMs).toISOString(),
sourceNotificationReceivedAt: trace.sourceNotificationReceivedAtMs == null
? undefined
: new Date(trace.sourceNotificationReceivedAtMs).toISOString(),
sourceNotificationReceivedAtMs: trace.sourceNotificationReceivedAtMs,
stagesMs: trace.stages, stagesMs: trace.stages,
})); }));
} }
+4 -1
View File
@@ -28,6 +28,7 @@ export interface IJobTemplate {
jobType: 'full' | 'empty'; jobType: 'full' | 'empty';
payoutMode: PayoutMode | 'all'; payoutMode: PayoutMode | 'all';
notificationEventId?: string; notificationEventId?: string;
sourceNotificationReceivedAtMs?: number;
notificationPublishedAtMs?: number; notificationPublishedAtMs?: number;
payoutSnapshotId?: string; payoutSnapshotId?: string;
payoutOutputs?: AddressObject[]; payoutOutputs?: AddressObject[];
@@ -150,6 +151,7 @@ export class StratumV1JobsService {
clearJobs, clearJobs,
isNewBlock, isNewBlock,
notificationEventId: blockTemplate.notificationEventId, notificationEventId: blockTemplate.notificationEventId,
sourceNotificationReceivedAtMs: blockTemplate.sourceNotificationReceivedAtMs,
notificationPublishedAtMs: blockTemplate.notificationPublishedAtMs, notificationPublishedAtMs: blockTemplate.notificationPublishedAtMs,
rawTransactions: blockTemplate.transactions, rawTransactions: blockTemplate.transactions,
sigoplimit: blockTemplate.sigoplimit, sigoplimit: blockTemplate.sigoplimit,
@@ -159,7 +161,7 @@ export class StratumV1JobsService {
}; };
}), }),
filter(next => next != null), filter(next => next != null),
map(({ prepared, timestamp, networkDifficulty, clearJobs, isNewBlock, notificationEventId, notificationPublishedAtMs, rawTransactions, sigoplimit, sizelimit, weightlimit, requiredVersionBits }) => { map(({ prepared, timestamp, networkDifficulty, clearJobs, isNewBlock, notificationEventId, sourceNotificationReceivedAtMs, notificationPublishedAtMs, rawTransactions, sigoplimit, sizelimit, weightlimit, requiredVersionBits }) => {
const block = new bitcoinjs.Block(); const block = new bitcoinjs.Block();
// Keep only a placeholder coinbase on the hot path. The full raw body // Keep only a placeholder coinbase on the hot path. The full raw body
@@ -198,6 +200,7 @@ export class StratumV1JobsService {
jobType: prepared.jobType, jobType: prepared.jobType,
payoutMode: prepared.payoutMode, payoutMode: prepared.payoutMode,
notificationEventId, notificationEventId,
sourceNotificationReceivedAtMs,
notificationPublishedAtMs, notificationPublishedAtMs,
payoutSnapshotId: prepared.coinbase.payoutSnapshotId, payoutSnapshotId: prepared.coinbase.payoutSnapshotId,
payoutOutputs: prepared.coinbase.payoutOutputs?.map(output => ({ ...output })), payoutOutputs: prepared.coinbase.payoutOutputs?.map(output => ({ ...output })),
+3
View File
@@ -513,6 +513,9 @@ export class StratumV1Service implements OnModuleInit, OnModuleDestroy {
console.log(JSON.stringify({ console.log(JSON.stringify({
event: 'stratum_job_fanout', event: 'stratum_job_fanout',
eventId: jobTemplate.blockData.notificationEventId, eventId: jobTemplate.blockData.notificationEventId,
sourceToFanoutStartMs: jobTemplate.blockData.sourceNotificationReceivedAtMs == null
? undefined
: Date.now() - jobTemplate.blockData.sourceNotificationReceivedAtMs,
redisToFanoutStartMs: jobTemplate.blockData.notificationPublishedAtMs == null redisToFanoutStartMs: jobTemplate.blockData.notificationPublishedAtMs == null
? undefined ? undefined
: Date.now() - jobTemplate.blockData.notificationPublishedAtMs, : Date.now() - jobTemplate.blockData.notificationPublishedAtMs,