Adds per-job timing metrics (#605)

This commit is contained in:
Chris Anderson 2025-01-05 15:56:53 -06:00 committed by GitHub
parent 1c262945f3
commit a5a82c8473
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 70 additions and 30 deletions

View file

@ -41,7 +41,7 @@ export class Stats {
const results = (await multi.exec()) ?? []
return results.map(([_, value]: any, index) => ({
date: new Date(keys[index] * 1000),
date: keys[index] * 1000,
count: parseInt(value ?? 0),
}))
}

View file

@ -35,16 +35,36 @@ router.get('/performance/jobs', async ctx => {
router.get('/performance/jobs/:job', async ctx => {
const jobName = ctx.params.job
ctx.body = [
{
label: 'added',
data: await App.main.stats.list(jobName),
},
{
label: 'completed',
data: await App.main.stats.list(`${jobName}:completed`),
},
]
const added = await App.main.stats.list(jobName)
const completed = await App.main.stats.list(`${jobName}:completed`)
const duration = await App.main.stats.list(`${jobName}:duration`)
const average = duration.map((item, index) => {
const count = completed[index]?.count
return {
date: item.date,
count: count ? item.count / count : 0,
}
})
ctx.body = {
throughput: [
{
label: 'added',
data: added,
},
{
label: 'completed',
data: completed,
},
],
timing: [
{
label: 'average',
data: average,
},
],
}
})
router.get('/performance/failed', async ctx => {

View file

@ -36,9 +36,10 @@ export default class Queue {
App.main.error.notify(new Error(`No handler found for job: ${job.name}`))
}
const start = Date.now()
await this.started(job)
await handler(job.data, job)
await this.completed(job)
await this.completed(job, Date.now() - start)
return true
}
@ -80,9 +81,11 @@ export default class Queue {
App.main.error.notify(error, job)
}
async completed(job: EncodedJob) {
async completed(job: EncodedJob, duration: number) {
logger.trace(job, 'queue:job:completed')
await App.main.stats.increment(`${job.name}:completed`)
await App.main.stats.increment(`${job.name}:duration`, duration)
}
async start() {

View file

@ -17,6 +17,7 @@
"api_keys": "API Keys",
"api_triggered": "API Triggered",
"archive": "Archive",
"average": "Average",
"back": "Back",
"balancer": "Balancer",
"balancer_desc": "Randomly split users across paths and rate limit traffic.",

View file

@ -17,6 +17,7 @@
"api_keys": "Claves API",
"api_triggered": "Activado por API",
"archive": "Archivar",
"average": "Promedio",
"back": "Atrás",
"balancer": "Equilibrador",
"balancer_desc": "Dividir aleatoriamente a los usuarios en rutas y limitar la velocidad del tráfico.",

View file

@ -334,7 +334,7 @@ const api = {
.get<string[]>('/admin/organizations/performance/jobs')
.then(r => r.data),
jobPerformance: async (job: string) => await client
.get<Series[]>(`/admin/organizations/performance/jobs/${job}`)
.get<{ throughput: Series[], timing: Series[] }>(`/admin/organizations/performance/jobs/${job}`)
.then(r => r.data),
failed: async () => await client
.get<any>('/admin/organizations/performance/failed')

View file

@ -14,7 +14,11 @@ import { PreferencesContext } from '../../ui/PreferencesContext'
import { formatDate } from '../../utils'
import { useTranslation } from 'react-i18next'
const Chart = ({ series }: { series: Series[] }) => {
interface ChartProps {
series: Series[]
formatter?: (value: number) => string
}
const Chart = ({ series, formatter = (value) => value.toLocaleString() }: ChartProps) => {
const strokes = ['#3C82F6', '#12B981']
const [preferences] = useContext(PreferencesContext)
return (
@ -34,7 +38,7 @@ const Chart = ({ series }: { series: Series[] }) => {
tick={{ fontSize: 12 }}
tickCount={15}
tickMargin={8} />
<YAxis tick={{ fontSize: 12 }} tickFormatter={count => count.toLocaleString() } />
<YAxis tick={{ fontSize: 12 }} tickFormatter={count => formatter(count) } />
<CartesianGrid vertical={false} />
<Tooltip
contentStyle={{
@ -42,7 +46,7 @@ const Chart = ({ series }: { series: Series[] }) => {
border: '1px solid var(--color-grey)',
}}
labelFormatter={(date: number) => formatDate(preferences, date)}
formatter={(value, name) => [`${name}: ${value.toLocaleString()}`]}
formatter={(value: any, name) => [`${name}: ${formatter(value)}`]}
/>
<Legend />
{series.map((s, index) => (
@ -61,6 +65,11 @@ const Chart = ({ series }: { series: Series[] }) => {
)
}
interface JobPerformance {
throughput: Series[]
timing: Series[]
}
export default function Performance() {
const { t } = useTranslation()
const [waiting, setWaiting] = useState(0)
@ -72,7 +81,7 @@ export default function Performance() {
const [jobs, setJobs] = useState<string[]>([])
const [currentJob, setCurrentJob] = useState<string | undefined>(job)
const [jobMetrics, setJobMetrics] = useState<Series[] | undefined>()
const [jobMetrics, setJobMetrics] = useState<JobPerformance | undefined>()
const [failed, setFailed] = useState<Array<Record<string, any>>>([])
const [selectedFailed, setSelectedFailed] = useState<Record<string, any> | undefined>()
@ -109,15 +118,10 @@ export default function Performance() {
useEffect(() => {
currentJob && api.organizations.jobPerformance(currentJob)
.then((series) => {
setJobMetrics(
series.map(({ data, label }) => ({
label: t(label),
data: data.map(item => ({
date: Date.parse(item.date as string),
count: item.count,
})),
})),
)
setJobMetrics({
throughput: series.throughput.map(({ data, label }) => ({ data, label: t(label) })),
timing: series.timing.map(({ data, label }) => ({ data, label: t(label) })),
})
})
.catch(() => {})
}, [currentJob])
@ -135,7 +139,8 @@ export default function Performance() {
title="Performance"
desc="View queue throughput for your project."
>
<Heading size="h4" title="Queue Throughput" />
<Heading size="h3" title="Queue" />
<Heading size="h4" title="Throughput" />
{metrics && <div>
<TileGrid numColumns={4}>
<Tile title={waiting.toLocaleString()} size="large">In Queue</Tile>
@ -143,7 +148,7 @@ export default function Performance() {
<Chart series={metrics} />
</div>}
<br /><br />
<Heading size="h4" title="Per Job Metrics" actions={
<Heading size="h3" title="Individual Job" actions={
<SingleSelect
size="small"
options={jobs}
@ -151,7 +156,17 @@ export default function Performance() {
onChange={handleChangeJob}
/>
} />
{jobMetrics && <Chart series={jobMetrics} />}
{jobMetrics && (
<>
<Heading size="h4" title="Throughput" />
<Chart series={jobMetrics.throughput} />
<Heading size="h4" title="Timing" />
<Chart
series={jobMetrics.timing}
formatter={(value) => `${value.toLocaleString()}ms`}
/>
</>
)}
{failed.length && <>
<Heading size="h4" title="Failed Jobs" />