diff --git a/lib/metrics.js b/lib/metrics.js new file mode 100644 index 0000000..65439ab --- /dev/null +++ b/lib/metrics.js @@ -0,0 +1,61 @@ +/** + * Tracks per-round retrieval metrics for SPARK: + * + * - Total number of retrieval attempts + * - Number of failed retrievals + * - Number of unique (PayloadCID, SP) pairs attempted + */ + +class RetrievalMetrics { + constructor() { + this.roundIndex = 0 + this.retrievalsTotal = 0 + this.retrievalsFailed = 0 + this.uniquePairs = new Set() + } + + /** Called at the start of a new SPARK round. */ + reset() { + this.retrievalsTotal = 0 + this.retrievalsFailed = 0 + this.uniquePairs.clear() + this.roundIndex++ + } + + /** + * Register a successful or attempted retrieval. + * + * @param {string} payloadCID + * @param {string | number} storageProvider + */ + recordRetrieval(payloadCID, storageProvider) { + this.retrievalsTotal += 1 + const key = `${payloadCID}:${storageProvider}` + this.uniquePairs.add(key) + } + + /** Must be called separately on error). */ + recordFailure() { + this.retrievalsFailed += 1 + } + + /** Log metrics for the current round. */ + report() { + console.log(`[METRICS] Round #${this.roundIndex}`) + console.log(` Retrievals attempted: ${this.retrievalsTotal}`) + console.log(` Retrievals failed: ${this.retrievalsFailed}`) + console.log(` Unique (PayloadCID, SP) pairs: ${this.uniquePairs.size}`) + } + + /** Get snapshot of current state. */ + getSnapshot() { + return { + round: this.roundIndex, + total: this.retrievalsTotal, + failed: this.retrievalsFailed, + uniquePairCount: this.uniquePairs.size, + } + } +} + +export const retrievalMetrics = new RetrievalMetrics() diff --git a/lib/spark.js b/lib/spark.js index 86e725d..f5bb4d1 100644 --- a/lib/spark.js +++ b/lib/spark.js @@ -13,6 +13,7 @@ import { assertOkResponse } from './http-assertions.js' import { getIndexProviderPeerId as defaultGetIndexProvider } from './miner-info.js' import { multiaddrToHttpUrl } from './multiaddr.js' import { Tasker } from './tasker.js' +import { retrievalMetrics } from './metrics.js' import { CarBlockIterator, @@ -255,16 +256,26 @@ export default class Spark { } const stats = newStats() + retrievalMetrics.recordRetrieval(retrieval.cid, retrieval.minerId) - await this.executeRetrievalCheck(retrieval, stats) - - const measurementId = await this.submitMeasurement(retrieval, { ...stats }) - Zinnia.jobCompleted() - return measurementId + //Track failed attempts + try { + await this.executeRetrievalCheck(retrieval, stats) + const measurementId = await this.submitMeasurement(retrieval, { + ...stats, + }) + Zinnia.jobCompleted() + return measurementId + } catch (err) { + retrievalMetrics.recordFailure() + throw err + } } async run() { while (true) { + retrievalMetrics.reset() + const started = Date.now() try { await this.nextRetrieval() @@ -288,6 +299,7 @@ export default class Spark { await sleep(delay) console.log() // add an empty line to visually delimit logs from different tasks } + retrievalMetrics.report() } } diff --git a/test/retrieval-metrics.test.js b/test/retrieval-metrics.test.js new file mode 100644 index 0000000..72e3115 --- /dev/null +++ b/test/retrieval-metrics.test.js @@ -0,0 +1,47 @@ +import { test } from 'zinnia:test' +import { assertEquals } from 'zinnia:assert' +import { retrievalMetrics } from '../lib/metrics.js' + +test('retrievalMetrics resets correctly', () => { + retrievalMetrics.reset() + const snap = retrievalMetrics.getSnapshot() + + assertEquals(snap.total, 0) + assertEquals(snap.failed, 0) + assertEquals(snap.uniquePairCount, 0) +}) + +test('retrievalMetrics counts retrievals and failures', () => { + retrievalMetrics.reset() + + retrievalMetrics.recordRetrieval('cid1', 'sp1') + retrievalMetrics.recordRetrieval('cid2', 'sp2') + retrievalMetrics.recordFailure() + + const snap = retrievalMetrics.getSnapshot() + + assertEquals(snap.total, 2) + assertEquals(snap.failed, 1) + assertEquals(snap.uniquePairCount, 2) +}) + +test('retrievalMetrics deduplicates (PayloadCID, SP) pairs', () => { + retrievalMetrics.reset() + + retrievalMetrics.recordRetrieval('cid1', 'sp1') + retrievalMetrics.recordRetrieval('cid1', 'sp1') + retrievalMetrics.recordRetrieval('cid2', 'sp2') + + const snap = retrievalMetrics.getSnapshot() + + assertEquals(snap.total, 3) + assertEquals(snap.uniquePairCount, 2) +}) + +test('retrievalMetrics roundIndex increments on reset', () => { + const before = retrievalMetrics.roundIndex + retrievalMetrics.reset() + const after = retrievalMetrics.roundIndex + + assertEquals(after, before + 1) +})