import { BaseCommand, flags } from '@adonisjs/core/ace' import type { CommandOptions } from '@adonisjs/core/types/ace' import db from '@adonisjs/lucid/services/db' import { decodeChunk } from '#lib/chunks/index' /** * For telling uploads apart (re-uploads under one release key): how long * one upload takes to post (the span of a release's files' dates, in * releases without a file name twice), and how far apart the copies of a * file name are in releases that have one twice. */ export default class ReleasesUploadGaps extends BaseCommand { static commandName = 'releases:upload-gaps' static description = 'Posting spans of uploads and gaps between re-uploads, in a sample' static options: CommandOptions = { startApp: true } @flags.number({ description: 'Releases sampled (with more than one file)', default: 20000 }) declare sample: number async run() { const ids: string[] = (await db.rawQuery(`SELECT id FROM releases TABLESAMPLE SYSTEM (5) WHERE files > 1 LIMIT ?`, [this.sample])).rows.map((r: { id: string }) => r.id) const bins = [10, 60, 180, 360, 720, 1440, 4320, Number.POSITIVE_INFINITY] const label = (m: number) => (m === Number.POSITIVE_INFINITY ? '>3d' : m >= 1440 ? `≤${m / 1440}d` : m >= 60 ? `≤${m / 60}h` : `≤${m}min`) const spans = new Array(bins.length).fill(0) const gaps = new Array(bins.length).fill(0) const bin = (minutes: number) => bins.findIndex((b) => minutes <= b) for (const id of ids) { const chunks = await db.from('release_chunks').where('release_id', id).select('payload') const byName = new Map() const times: number[] = [] for (const c of chunks) { for (const f of decodeChunk(c.payload)) { if (f.postedAt === null) continue times.push(f.postedAt) byName.set(f.name, [...(byName.get(f.name) ?? []), f.postedAt]) } } if (times.length < 2) continue const twice = [...byName.values()].filter((t) => t.length > 1) if (twice.length === 0) { spans[bin((Math.max(...times) - Math.min(...times)) / 60_000)]++ continue } // The gap between a name's copies (sorted; each step), so a release reposted 3 times counts 2 gaps per name. for (const t of twice) { t.sort((a, b) => a - b) for (let i = 1; i < t.length; i++) gaps[bin((t[i] - t[i - 1]) / 60_000)]++ } } const show = (counts: number[]) => { const total = counts.reduce((a, b) => a + b, 0) let cumulative = 0 return bins.map((b, i) => `${label(b)} ${((100 * (cumulative += counts[i])) / Math.max(1, total)).toFixed(1)}%`).join(', ') + ` (n=${total})` } this.logger.info(`one upload's posting span (cumulative): ${show(spans)}`) this.logger.info(`gap between copies of a name (cumulative): ${show(gaps)}`) } }