Skip to content

feat: track and report per-round retrieval metrics (attempts, failures, coverage) #136

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
61 changes: 61 additions & 0 deletions lib/metrics.js
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 17 additions & 5 deletions lib/spark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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()
}
}

Expand Down
47 changes: 47 additions & 0 deletions test/retrieval-metrics.test.js
Original file line number Diff line number Diff line change
@@ -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)
})