Skip to content
Merged
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
232 changes: 232 additions & 0 deletions src/__benchmarks__/update-player-mmr.benchmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { Pool } from 'pg'

const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})

type BenchmarkResult = {
name: string
duration: number
operations: number
opsPerSecond: number
memoryUsed: number
}

async function updatePlayerMmrIndividual(
queueId: number,
userId: string,
newElo: number,
newVolatility: number,
): Promise<void> {
const clampedElo = Math.max(0, Math.min(9999, newElo))
await pool.query(
`UPDATE queue_users SET elo = $1, peak_elo = GREATEST(peak_elo, $1), volatility = $2 WHERE user_id = $3 AND queue_id = $4`,
[clampedElo, newVolatility, userId, queueId],
)
}

async function updatePlayerMmrBulk(
queueId: number,
updates: Array<{ user_id: string; elo: number; volatility: number }>,
): Promise<void> {
if (updates.length === 0) return

const values = updates.flatMap((u) => [
u.elo,
u.volatility,
u.user_id,
queueId,
])

const placeholders = updates
.map((_, i) => {
const offset = i * 4
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4})`
})
.join(', ')

await pool.query(
`UPDATE queue_users AS qu
SET elo = v.elo::numeric,
peak_elo = GREATEST(qu.peak_elo, v.elo::numeric),
volatility = v.volatility::integer
FROM (VALUES ${placeholders}) AS v(elo, volatility, user_id, queue_id)
WHERE qu.user_id = v.user_id::text AND qu.queue_id = v.queue_id::integer`,
values,
)
}

async function benchmark(
name: string,
fn: () => Promise<void>,
operations: number,
iterations: number = 1,
): Promise<BenchmarkResult> {
const startMem = process.memoryUsage().heapUsed
const durations: number[] = []

for (let i = 0; i < iterations; i++) {
const start = performance.now()
await fn()
const end = performance.now()
durations.push(end - start)
}

const endMem = process.memoryUsage().heapUsed
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length

return {
name,
duration: avgDuration,
operations,
opsPerSecond: (operations / avgDuration) * 1000,
memoryUsed: endMem - startMem,
}
}

async function getExistingPlayers(
queueId: number,
limit: number,
): Promise<string[]> {
const result = await pool.query(
`SELECT user_id FROM queue_users WHERE queue_id = $1 LIMIT $2`,
[queueId, limit],
)
return result.rows.map((row) => row.user_id)
}

async function getQueueId(): Promise<number> {
const result = await pool.query(`SELECT id FROM queues LIMIT 1`)
if (result.rows.length === 0) {
throw new Error('No queues found in database')
}
return result.rows[0].id
}

async function runBenchmark(
name: string,
queueId: number,
playerCount: number,
method: 'individual' | 'bulk',
iterations: number = 3,
): Promise<BenchmarkResult> {
console.log(`\nRunning: ${name}...`)

const userIds = await getExistingPlayers(queueId, playerCount)

if (userIds.length < playerCount) {
console.log(`⚠ Only found ${userIds.length} players, using that instead`)
}

const result = await benchmark(
name,
async () => {
if (method === 'individual') {
for (const userId of userIds) {
await updatePlayerMmrIndividual(
queueId,
userId,
1000 + Math.random() * 50,
Math.floor(Math.random() * 10),
)
}
} else {
const updates = userIds.map((userId) => ({
user_id: userId,
elo: 1000 + Math.random() * 50,
volatility: Math.floor(Math.random() * 10),
}))
await updatePlayerMmrBulk(queueId, updates)
}
},
userIds.length,
iterations,
)

console.log(
`✓ Completed: ${result.duration.toFixed(2)}ms (${result.opsPerSecond.toFixed(2)} ops/sec)`,
)
return result
}

async function main() {
console.log('=== MMR Update Benchmark ===\n')
console.log('Warming up connection...\n')

await pool.query('SELECT 1')

const queueId = await getQueueId()
console.log(`Using queue_id: ${queueId}`)

const totalPlayers = await pool.query(
`SELECT COUNT(*) FROM queue_users WHERE queue_id = $1`,
[queueId],
)
console.log(`Total players in queue: ${totalPlayers.rows[0].count}`)
console.log('Iterations per test: 3\n')

const results: BenchmarkResult[] = []

const testCases = [
{ players: 10, label: '10 players' },
{ players: 50, label: '50 players' },
{ players: 100, label: '100 players' },
{ players: 500, label: '500 players' },
{ players: 1000, label: '1000 players' },
]

for (const testCase of testCases) {
results.push(
await runBenchmark(
`${testCase.label} - individual`,
queueId,
testCase.players,
'individual',
),
)

results.push(
await runBenchmark(
`${testCase.label} - bulk`,
queueId,
testCase.players,
'bulk',
),
)
}

console.log('\n\n=== Results ===\n')
console.table(
results.map((r) => ({
Test: r.name,
'Avg Duration (ms)': r.duration.toFixed(2),
Ops: r.operations,
'Ops/sec': r.opsPerSecond.toFixed(2),
'Memory (KB)': (r.memoryUsed / 1024).toFixed(2),
})),
)

console.log('\n=== Performance Comparison ===\n')
for (let i = 0; i < results.length; i += 2) {
const individual = results[i]
const bulk = results[i + 1]
const speedup = individual.duration / bulk.duration

console.log(`${individual.name.split(' - ')[0]}:`)
console.log(` Bulk is ${speedup.toFixed(2)}x faster`)
console.log(
` Time saved: ${(individual.duration - bulk.duration).toFixed(2)}ms`,
)
console.log(
` Memory diff: ${((bulk.memoryUsed - individual.memoryUsed) / 1024).toFixed(2)}KB\n`,
)
}

await pool.end()
console.log('Benchmark complete.')
}

main().catch((error) => {
console.error('Benchmark failed:', error)
pool.end().finally(() => process.exit(1))
})
41 changes: 28 additions & 13 deletions src/utils/algorithms/calculateMMR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
getQueueSettings,
getUsersNeedingRoleUpdates,
updatePlayerMmrAll,
updatePlayerMmrBulk,
} from '../queryDB'
import type { Matches, Queues, teamResults } from 'psqlDB'
import { setUserQueueRole } from 'utils/queueHelpers'
Expand Down Expand Up @@ -143,39 +144,53 @@ export async function calculateNewMMR(
queueSettings.default_elo,
)

const playerMMRChanges: Array<{ user_id: string; oldMMR: number; newMMR: number }> = []
const updatePromises: Promise<void>[] = []
const playerMMRChanges: Array<{
user_id: string
oldMMR: number
newMMR: number
}> = []

const playerUpdates: Array<{
user_id: string
elo: number
volatility: number
}> = []

for (const ts of teamStats) {
const isWinner = ts.isWinner
const mmrChange = isWinner ? ratingChange : -ratingChange / loserCount
const mmrChange = ts.isWinner ? ratingChange : -ratingChange / loserCount
const mmrChangeRounded = Math.round(mmrChange * 10) / 10

for (const player of ts.team.players) {
const oldMMR = player.elo ?? queueSettings.default_elo
const oldVolatility = player.volatility ?? 0

const newMMR = parseFloat((oldMMR + mmrChange).toFixed(1))
const newMMR = Math.round((oldMMR + mmrChange) * 10) / 10
const clampedMMR = Math.max(0, Math.min(9999, newMMR))
const newVolatility = Math.min(oldVolatility + 1, 10)

playerMMRChanges.push({
user_id: player.user_id,
oldMMR,
newMMR,
newMMR: clampedMMR,
})

player.elo = clamp(newMMR, 0, 9999)
player.elo_change = parseFloat(mmrChange.toFixed(1))
player.elo = clampedMMR
player.elo_change = mmrChangeRounded
player.volatility = newVolatility

updatePromises.push(
updatePlayerMmrAll(queueId, player.user_id, newMMR, newVolatility),
)
playerUpdates.push({
user_id: player.user_id,
elo: clampedMMR,
volatility: newVolatility,
})
}

ts.team.score = isWinner ? 1 : 0
ts.team.score = ts.isWinner ? 1 : 0
}

await Promise.all(updatePromises)
if (playerUpdates.length > 0) {
await updatePlayerMmrBulk(queueId, playerUpdates)
}

const usersNeedingRoleUpdate = await getUsersNeedingRoleUpdates(
queueId,
Expand Down
35 changes: 32 additions & 3 deletions src/utils/queryDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,12 @@ export async function getUsersNeedingRoleUpdates(
[queueId],
)

const thresholds = roles.rows.map(r => r.mmr_threshold)
const thresholds = roles.rows.map((r) => r.mmr_threshold)
const usersToUpdate: string[] = []

for (const player of players) {
const oldRole = thresholds.find(t => t <= player.oldMMR)
const newRole = thresholds.find(t => t <= player.newMMR)
const oldRole = thresholds.find((t) => t <= player.oldMMR)
const newRole = thresholds.find((t) => t <= player.newMMR)

if (oldRole !== newRole) {
usersToUpdate.push(player.user_id)
Expand Down Expand Up @@ -1084,6 +1084,35 @@ export async function updatePlayerMmrAll(
)
}

export async function updatePlayerMmrBulk(
queueId: number,
updates: Array<{ user_id: string; elo: number; volatility: number }>,
): Promise<void> {
const values = updates.flatMap((u) => [
u.elo,
u.volatility,
u.user_id,
queueId,
])

const placeholders = updates
.map((_, i) => {
const offset = i * 4
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4})`
})
.join(', ')

await pool.query(
`UPDATE queue_users AS qu
SET elo = v.elo::numeric,
peak_elo = GREATEST(qu.peak_elo, v.elo::numeric),
volatility = v.volatility::integer
FROM (VALUES ${placeholders}) AS v(elo, volatility, user_id, queue_id)
WHERE qu.user_id = v.user_id::text AND qu.queue_id = v.queue_id::integer`,
values,
)
}

export async function updatePlayerElo(
queueId: number,
userId: string,
Expand Down