Skip to content

Commit 6a14237

Browse files
authored
Add routes for time-to-first-byte stats (#281)
* Add routes for time-to-first-byte stats * Make retrieval time stats more generic * Add tests for retrieval times * Refactor code to use retrieval timings table * Use more odd values in retrieval timing tests * Manually update spark-evaluate dep inside db workspace * Update spark-evaluate dependency in all workspaces
1 parent 611c616 commit 6a14237

File tree

4 files changed

+128
-4
lines changed

4 files changed

+128
-4
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stats/lib/handler.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
fetchRetrievalSuccessRate,
1515
fetchDealSummary,
1616
fetchDailyRetrievalResultCodes,
17-
fetchDailyMinerRSRSummary
17+
fetchDailyMinerRSRSummary,
18+
fetchDailyRetrievalTimings,
19+
fetchDailyMinerRetrievalTimings
1820
} from './stats-fetchers.js'
1921

2022
import { handlePlatformRoutes } from './platform-routes.js'
@@ -108,6 +110,10 @@ const handler = async (req, res, pgPools, SPARK_API_BASE_URL) => {
108110
await respond(fetchMinersRSRSummary)
109111
} else if (req.method === 'GET' && url === '/retrieval-result-codes/daily') {
110112
await respond(fetchDailyRetrievalResultCodes)
113+
} else if (req.method === 'GET' && url === '/retrieval-timings/daily') {
114+
await respond(fetchDailyRetrievalTimings)
115+
} else if (req.method === 'GET' && segs[0] === 'miner' && segs[1] && segs[2] === 'retrieval-timings' && segs[3] === 'summary') {
116+
await respond(fetchDailyMinerRetrievalTimings, segs[1])
111117
} else if (req.method === 'GET' && segs[0] === 'miner' && segs[1] && segs[2] === 'retrieval-success-rate' && segs[3] === 'summary') {
112118
await respond(fetchDailyMinerRSRSummary, segs[1])
113119
} else if (req.method === 'GET' && segs[0] === 'miner' && segs[1] && segs[2] === 'deals' && segs[3] === 'eligible' && segs[4] === 'summary') {

stats/lib/stats-fetchers.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,48 @@ export const fetchDailyRetrievalResultCodes = async (pgPools, filter) => {
290290
const stats = Object.entries(days).map(([day, rates]) => ({ day, rates }))
291291
return stats
292292
}
293+
294+
/**
295+
* Fetches daily global retrieval time statistics
296+
* @param {import('@filecoin-station/spark-stats-db').PgPools} pgPools
297+
* @param {import('./typings.js').DateRangeFilter} filter
298+
*/
299+
export const fetchDailyRetrievalTimings = async (pgPools, filter) => {
300+
const { rows } = await pgPools.evaluate.query(`
301+
SELECT
302+
day::text,
303+
CEIL(percentile_cont(0.5) WITHIN GROUP (ORDER BY ttfb_p50_values)) AS ttfb_ms
304+
FROM retrieval_timings, UNNEST(ttfb_p50) AS ttfb_p50_values
305+
WHERE day >= $1 AND day <= $2
306+
GROUP BY day
307+
ORDER BY day
308+
`, [
309+
filter.from,
310+
filter.to
311+
])
312+
return rows
313+
}
314+
315+
/**
316+
* Fetches per miner daily retrieval time statistics
317+
* @param {import('@filecoin-station/spark-stats-db').PgPools} pgPools
318+
* @param {import('./typings.js').DateRangeFilter} filter
319+
* @param {string} minerId
320+
*/
321+
export const fetchDailyMinerRetrievalTimings = async (pgPools, { from, to }, minerId) => {
322+
const { rows } = await pgPools.evaluate.query(`
323+
SELECT
324+
day::text,
325+
miner_id,
326+
CEIL(percentile_cont(0.5) WITHIN GROUP (ORDER BY ttfb_p50_values)) AS ttfb_ms
327+
FROM retrieval_timings, UNNEST(ttfb_p50) AS ttfb_p50_values
328+
WHERE miner_id = $1 AND day >= $2 AND day <= $3
329+
GROUP BY day, miner_id
330+
ORDER BY day
331+
`, [
332+
minerId,
333+
from,
334+
to
335+
])
336+
return rows
337+
}

stats/test/handler.test.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe('HTTP request handler', () => {
4848
await pgPools.evaluate.query('DELETE FROM retrieval_stats')
4949
await pgPools.evaluate.query('DELETE FROM daily_participants')
5050
await pgPools.evaluate.query('DELETE FROM daily_deals')
51+
await pgPools.evaluate.query('DELETE FROM retrieval_timings')
5152
await pgPools.stats.query('DELETE FROM daily_scheduled_rewards')
5253
await pgPools.stats.query('DELETE FROM daily_reward_transfers')
5354
await pgPools.stats.query('DELETE FROM daily_retrieval_result_codes')
@@ -767,6 +768,63 @@ describe('HTTP request handler', () => {
767768
])
768769
})
769770
})
771+
772+
describe('miner retrieval timing stats', () => {
773+
beforeEach(async () => {
774+
// before the range
775+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-09', minerId: 'f1one', timeToFirstByteP50: [1000] })
776+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-09', minerId: 'f1two', timeToFirstByteP50: [1000] })
777+
// in the range
778+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-20', minerId: 'f1one', timeToFirstByteP50: [1000] })
779+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-20', minerId: 'f1two', timeToFirstByteP50: [1000] })
780+
781+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-10', minerId: 'f1one', timeToFirstByteP50: [123, 345] })
782+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-10', minerId: 'f1two', timeToFirstByteP50: [654, 789] })
783+
// after the range
784+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-21', minerId: 'f1one', timeToFirstByteP50: [1000] })
785+
await givenRetrievalTimings(pgPools.evaluate, { day: '2024-01-21', minerId: 'f1two', timeToFirstByteP50: [1000] })
786+
})
787+
788+
it('lists daily retrieval timings in given date range', async () => {
789+
const res = await fetch(
790+
new URL(
791+
'/retrieval-timings/daily?from=2024-01-10&to=2024-01-20',
792+
baseUrl
793+
), {
794+
redirect: 'manual'
795+
}
796+
)
797+
await assertResponseStatus(res, 200)
798+
799+
const stats = /** @type {{ day: string, success_rate: number }[]} */(
800+
await res.json()
801+
)
802+
assert.deepStrictEqual(stats, [
803+
{ day: '2024-01-10', ttfb_ms: 500 },
804+
{ day: '2024-01-20', ttfb_ms: 1000 }
805+
])
806+
})
807+
808+
it('lists daily retrieval timings summary for specified miner in given date range', async () => {
809+
const res = await fetch(
810+
new URL(
811+
'/miner/f1one/retrieval-timings/summary?from=2024-01-10&to=2024-01-20',
812+
baseUrl
813+
), {
814+
redirect: 'manual'
815+
}
816+
)
817+
await assertResponseStatus(res, 200)
818+
819+
const stats = /** @type {{ day: string, success_rate: number }[]} */(
820+
await res.json()
821+
)
822+
assert.deepStrictEqual(stats, [
823+
{ day: '2024-01-10', miner_id: 'f1one', ttfb_ms: 234 },
824+
{ day: '2024-01-20', miner_id: 'f1one', ttfb_ms: 1000 }
825+
])
826+
})
827+
})
770828
})
771829

772830
/**
@@ -843,3 +901,18 @@ const givenDailyDealStats = async (pgPool, {
843901
retrievable
844902
])
845903
}
904+
905+
/**
906+
*
907+
* @param {import('../lib/platform-stats-fetchers.js').Queryable} pgPool
908+
* @param {object} data
909+
* @param {string} data.day
910+
* @param {string} data.minerId
911+
* @param {number[]} data.timeToFirstByteP50
912+
*/
913+
const givenRetrievalTimings = async (pgPool, { day, minerId, timeToFirstByteP50 }) => {
914+
await pgPool.query(
915+
'INSERT INTO retrieval_timings (day, miner_id, ttfb_p50) VALUES ($1, $2, $3)',
916+
[day, minerId ?? 'f1test', timeToFirstByteP50]
917+
)
918+
}

0 commit comments

Comments
 (0)