Skip to content

Instantly share code, notes, and snippets.

@pyropy
Last active July 14, 2025 12:06
Show Gist options
  • Save pyropy/e6ea0456ae374cde2557d2d1ea90f7ac to your computer and use it in GitHub Desktop.
Save pyropy/e6ea0456ae374cde2557d2d1ea90f7ac to your computer and use it in GitHub Desktop.

Revisions

  1. pyropy revised this gist Jul 14, 2025. 1 changed file with 107 additions and 19 deletions.
    126 changes: 107 additions & 19 deletions measure-response-times.js
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,88 @@
    #!/usr/bin/env node
    // CLI to fetch a blob from a random aggregator and from 0x.mainnet.walcdn.io, measuring TTFB and TTLB
    import { getRandomAggregator } from '../retriever/lib/aggregators.js'
    import { base64UrlToBigInt } from '../retriever/lib/base64.js'

    const AGGREGATORS = [
    'https://agg.walrus.eosusa.io',
    'https://aggregator.mainnet.walrus.mirai.cloud',
    'https://aggregator.suicore.com',
    'https://aggregator.walrus-mainnet.tududes.com',
    'https://aggregator.walrus-mainnet.walrus.space',
    'https://aggregator.walrus.atalma.io',
    'https://aggregator.walrus.mainnet.mozcomputing.dev',
    'https://aggregator.walrus.silentvalidator.com',
    'https://mainnet-aggregator.hoh.zone',
    'https://mainnet-aggregator.walrus.graphyte.dev',
    'https://mainnet-walrus-aggregator.kiliglab.io',
    'https://sm1-walrus-mainnet-aggregator.stakesquid.com',
    'https://sui-walrus-mainnet-aggregator.bwarelabs.com',
    'https://suiftly.mhax.io',
    'https://wal-aggregator-mainnet.staketab.org',
    'https://walmain.agg.chainflow.io',
    'https://walrus-agg.mainnet.obelisk.sh',
    'https://walrus-aggregator-mainnet.chainode.tech:9002',
    'https://walrus-aggregator.brightlystake.com',
    'https://walrus-aggregator.chainbase.online',
    'https://walrus-aggregator.n1stake.com',
    'https://walrus-aggregator.natsai.xyz',
    'https://walrus-aggregator.rockfin.io',
    'https://walrus-aggregator.rubynodes.io',
    'https://walrus-aggregator.stakely.io',
    'https://walrus-aggregator.stakin-nodes.com',
    'https://walrus-aggregator.staking4all.org',
    'https://walrus-aggregator.starduststaking.com',
    'https://walrus-aggregator.talentum.id',
    'https://walrus-aggregator.thcloud.ai',
    'https://walrus-aggregator.thepassivetrust.com',
    'https://walrus-cache-mainnet.latitude.sh',
    'https://walrus-cache-mainnet.overclock.run',
    'https://walrus-main-aggregator.4everland.org',
    'https://walrus-mainnet-aggregator-1.zkv.xyz',
    'https://walrus-mainnet-aggregator.crouton.digital',
    'https://walrus-mainnet-aggregator.dzdaic.com',
    'https://walrus-mainnet-aggregator.everstake.one',
    'https://walrus-mainnet-aggregator.imperator.co',
    'https://walrus-mainnet-aggregator.luckyresearch.org',
    'https://walrus-mainnet-aggregator.nami.cloud',
    'https://walrus-mainnet-aggregator.nodeinfra.com',
    'https://walrus-mainnet-aggregator.redundex.com',
    'https://walrus-mainnet-aggregator.rpc101.org',
    'https://walrus-mainnet-aggregator.stakecraft.com',
    'https://walrus-mainnet-aggregator.stakeengine.co.uk',
    'https://walrus-mainnet-aggregator.stakingdefenseleague.com.',
    'https://walrus-mainnet-aggregator.trusted-point.com',
    'https://walrus.aggregator.stakepool.dev.br',
    'https://walrus.blockscope.net',
    'https://walrus.globalstake.io',
    'https://walrus.lakestake.io',
    'https://walrus.lionscraft.blockscape.network:9000',
    'https://walrus.prostaking.com:9443',
    'https://walrus.veera.com',
    'https://walrusagg.pops.one',
    'http://walrus-aggregator.winsnip.site:9000',
    'http://walrus.equinoxdao.xyz:9000',
    'http://67.220.194.10:9000',
    ]

    /**
    * @param {bigint} [seed]
    * @returns
    */
    function getRandomAggregator(seed) {
    let idx

    if (seed !== undefined) {
    idx = Number(seed % BigInt(AGGREGATORS.length))
    } else {
    idx = Math.floor(Math.random() * AGGREGATORS.length)
    }

    return AGGREGATORS[idx]
    }


    function base64UrlToBigInt(base64url) {
    const hex = '0x' + Buffer.from(base64url, 'base64url').toString('hex')
    return BigInt(hex)
    }

    function nowMs() {
    const [s, ns] = process.hrtime()
    @@ -36,6 +117,13 @@ async function fetchWithTiming(url) {
    return stats
    }

    /**
    * Calculate percentiles for an array of values.
    *
    * @param {number[]} values
    * @param {number[]} percentiles
    * @returns {{`p${number}`: number}}
    */
    function calculatePercentiles(values, percentiles) {
    const sorted = values.slice().sort((a, b) => a - b)
    const result = {}
    @@ -55,7 +143,7 @@ function calculatePercentiles(values, percentiles) {
    return result
    }

    async function runMultipleTests(url, iterations = 10) {
    async function measureResponseTimes(url, iterations = 10) {
    console.log(`\nRunning ${iterations} iterations for: ${url}`)
    const results = {
    ttfb: [],
    @@ -117,39 +205,39 @@ async function main() {
    process.exit(1)
    }

    // 1. Random aggregator
    // 1. Random aggregator (cold start)
    const aggregator = getRandomAggregator()
    const aggUrl = `${aggregator}/v1/blobs/${blobId}`
    const aggResults = await runMultipleTests(aggUrl, numIterations)
    const aggResults = await measureResponseTimes(aggUrl, numIterations)
    printStats('Random Aggregator (cold start)', aggResults)

    // 2. Mainnet (warm up first to avoid cold start affecting all measurements)
    // 2. CDN (cold start)
    const mainnetUrl = `https://3rkdb89wnwzj3f2i7hnvuyna5vn7glk9vf3igtht9x6ejmboly.walcdn.io/${blobId}`

    const mainnetResults = await runMultipleTests(mainnetUrl, numIterations)
    const mainnetResults = await measureResponseTimes(mainnetUrl, numIterations)
    printStats('Mainnet (cold start)', mainnetResults)

    // 3. Random aggregator (warm up)
    fetchWithTiming(aggUrl) // Warm up
    const warmAggResults = await runMultipleTests(aggUrl, numIterations)
    // 3. Random aggregator (warmed up)
    await fetch(aggUrl) // Warm up
    const warmAggResults = await measureResponseTimes(aggUrl, numIterations)
    printStats('Random Aggregator (warm start)', warmAggResults)

    // 4. Mainnet (warm up)
    fetchWithTiming(mainnetUrl) // Warm up
    const warmMainnetResults = await runMultipleTests(mainnetUrl, numIterations)
    // 4. CDN (warmed up)
    await fetch (mainnetUrl) // Warm up
    const warmMainnetResults = await measureResponseTimes(mainnetUrl, numIterations)
    printStats('Mainnet (warm start)', warmMainnetResults)

    // 5. Random deterministic aggregator (cold start)
    // 5. Pseudo-random aggregator (cold start)
    const seed = base64UrlToBigInt(blobId)
    const deterministicAggregator = getRandomAggregator(seed)
    const detAggUrl = `${deterministicAggregator}/v1/blobs/${blobId}`
    const detAggResults = await runMultipleTests(detAggUrl, numIterations)
    const detAggResults = await measureResponseTimes(detAggUrl, numIterations)
    printStats('Pseudo-Random Aggregator (cold start)', detAggResults)

    // 6. Random deterministic aggregator (warm up)
    fetchWithTiming(detAggUrl) // Warm up
    // 6. Pseudo-random aggregator (warmed up)
    await fetch (detAggUrl) // Warm up

    const warmDetAggResults = await runMultipleTests(detAggUrl, numIterations)
    const warmDetAggResults = await measureResponseTimes(detAggUrl, numIterations)
    printStats('Pseudo-Random Aggregator (warm start)', warmDetAggResults)

    console.log('\nAll tests completed.')
  2. pyropy created this gist Jul 14, 2025.
    158 changes: 158 additions & 0 deletions measure-response-times.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,158 @@
    #!/usr/bin/env node
    // CLI to fetch a blob from a random aggregator and from 0x.mainnet.walcdn.io, measuring TTFB and TTLB
    import { getRandomAggregator } from '../retriever/lib/aggregators.js'
    import { base64UrlToBigInt } from '../retriever/lib/base64.js'

    function nowMs() {
    const [s, ns] = process.hrtime()
    return s * 1000 + ns / 1e6
    }

    async function fetchWithTiming(url) {
    const start = nowMs()
    const stats = {
    ttfb: null,
    ttlb: null,
    size: 0,
    status: null,
    headers: null,
    }

    const res = await fetch(url)
    stats.status = res.status
    stats.headers = res.headers
    const reader = res.body.getReader()
    let first = true
    while (true) {
    const t = await reader.read()
    if (first) {
    stats.ttfb = nowMs() - start
    first = false
    }
    if (t.done) break
    stats.size += t.value.length
    }
    stats.ttlb = nowMs() - start
    return stats
    }

    function calculatePercentiles(values, percentiles) {
    const sorted = values.slice().sort((a, b) => a - b)
    const result = {}

    for (const p of percentiles) {
    const index = (p / 100) * (sorted.length - 1)
    if (index % 1 === 0) {
    result[`p${p}`] = sorted[index]
    } else {
    const lower = Math.floor(index)
    const upper = Math.ceil(index)
    const weight = index - lower
    result[`p${p}`] = sorted[lower] * (1 - weight) + sorted[upper] * weight
    }
    }

    return result
    }

    async function runMultipleTests(url, iterations = 10) {
    console.log(`\nRunning ${iterations} iterations for: ${url}`)
    const results = {
    ttfb: [],
    ttlb: [],
    size: [],
    status: [],
    errors: 0
    }

    for (let i = 0; i < iterations; i++) {
    try {
    const stats = await fetchWithTiming(url)
    results.ttfb.push(stats.ttfb)
    results.ttlb.push(stats.ttlb)
    results.size.push(stats.size)
    results.status.push(stats.status)
    process.stdout.write(`\rProgress: ${i + 1}/${iterations}`)
    } catch (e) {
    results.errors++
    console.error(`\nIteration ${i + 1} failed:`, e.message)
    }
    }

    console.log() // New line after progress indicator
    return results
    }

    function printStats(label, results) {
    if (results.ttfb.length === 0) {
    console.log(`${label}: All requests failed`)
    return
    }

    const ttfbPercentiles = calculatePercentiles(results.ttfb, [50, 90, 99])
    const ttlbPercentiles = calculatePercentiles(results.ttlb, [50, 90, 99])
    const avgSize = results.size.reduce((a, b) => a + b, 0) / results.size.length
    const successRate = ((results.ttfb.length / (results.ttfb.length + results.errors)) * 100).toFixed(1)

    console.log(`${label}:`)
    console.log(` Success Rate: ${successRate}%`)
    console.log(` TTFB - p50: ${ttfbPercentiles.p50.toFixed(1)}ms, p90: ${ttfbPercentiles.p90.toFixed(1)}ms, p99: ${ttfbPercentiles.p99.toFixed(1)}ms`)
    console.log(` TTLB - p50: ${ttlbPercentiles.p50.toFixed(1)}ms, p90: ${ttlbPercentiles.p90.toFixed(1)}ms, p99: ${ttlbPercentiles.p99.toFixed(1)}ms`)
    console.log(` Average Size: ${avgSize.toFixed(0)} bytes`)
    if (results.status.length > 0) {
    console.log(` Status Codes: ${[...new Set(results.status)].join(', ')}`)
    }
    }

    async function main() {
    const [, , blobId, iterations = '10'] = process.argv
    if (!blobId) {
    console.error('Usage: measure-response-time.js <blobId> [iterations]')
    process.exit(1)
    }

    const numIterations = parseInt(iterations, 10)
    if (isNaN(numIterations) || numIterations < 1) {
    console.error('Iterations must be a positive number')
    process.exit(1)
    }

    // 1. Random aggregator
    const aggregator = getRandomAggregator()
    const aggUrl = `${aggregator}/v1/blobs/${blobId}`
    const aggResults = await runMultipleTests(aggUrl, numIterations)
    printStats('Random Aggregator (cold start)', aggResults)

    // 2. Mainnet (warm up first to avoid cold start affecting all measurements)
    const mainnetUrl = `https://3rkdb89wnwzj3f2i7hnvuyna5vn7glk9vf3igtht9x6ejmboly.walcdn.io/${blobId}`

    const mainnetResults = await runMultipleTests(mainnetUrl, numIterations)
    printStats('Mainnet (cold start)', mainnetResults)

    // 3. Random aggregator (warm up)
    fetchWithTiming(aggUrl) // Warm up
    const warmAggResults = await runMultipleTests(aggUrl, numIterations)
    printStats('Random Aggregator (warm start)', warmAggResults)

    // 4. Mainnet (warm up)
    fetchWithTiming(mainnetUrl) // Warm up
    const warmMainnetResults = await runMultipleTests(mainnetUrl, numIterations)
    printStats('Mainnet (warm start)', warmMainnetResults)

    // 5. Random deterministic aggregator (cold start)
    const seed = base64UrlToBigInt(blobId)
    const deterministicAggregator = getRandomAggregator(seed)
    const detAggUrl = `${deterministicAggregator}/v1/blobs/${blobId}`
    const detAggResults = await runMultipleTests(detAggUrl, numIterations)
    printStats('Pseudo-Random Aggregator (cold start)', detAggResults)

    // 6. Random deterministic aggregator (warm up)
    fetchWithTiming(detAggUrl) // Warm up

    const warmDetAggResults = await runMultipleTests(detAggUrl, numIterations)
    printStats('Pseudo-Random Aggregator (warm start)', warmDetAggResults)

    console.log('\nAll tests completed.')
    }

    main()