Skip to content

Commit a0c3b60

Browse files
committed
feat: add MMR update benchmarking script and bulk MMR update functionality
1 parent 7f6e945 commit a0c3b60

File tree

3 files changed

+293
-14
lines changed

3 files changed

+293
-14
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { Pool } from 'pg'
2+
3+
const pool = new Pool({
4+
connectionString: process.env.DATABASE_URL,
5+
})
6+
7+
type BenchmarkResult = {
8+
name: string
9+
duration: number
10+
operations: number
11+
opsPerSecond: number
12+
memoryUsed: number
13+
}
14+
15+
async function updatePlayerMmrIndividual(
16+
queueId: number,
17+
userId: string,
18+
newElo: number,
19+
newVolatility: number,
20+
): Promise<void> {
21+
const clampedElo = Math.max(0, Math.min(9999, newElo))
22+
await pool.query(
23+
`UPDATE queue_users SET elo = $1, peak_elo = GREATEST(peak_elo, $1), volatility = $2 WHERE user_id = $3 AND queue_id = $4`,
24+
[clampedElo, newVolatility, userId, queueId],
25+
)
26+
}
27+
28+
async function updatePlayerMmrBulk(
29+
queueId: number,
30+
updates: Array<{ user_id: string; elo: number; volatility: number }>,
31+
): Promise<void> {
32+
if (updates.length === 0) return
33+
34+
const values = updates.flatMap((u) => [
35+
u.elo,
36+
u.volatility,
37+
u.user_id,
38+
queueId,
39+
])
40+
41+
const placeholders = updates
42+
.map((_, i) => {
43+
const offset = i * 4
44+
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4})`
45+
})
46+
.join(', ')
47+
48+
await pool.query(
49+
`UPDATE queue_users AS qu
50+
SET elo = v.elo::numeric,
51+
peak_elo = GREATEST(qu.peak_elo, v.elo::numeric),
52+
volatility = v.volatility::integer
53+
FROM (VALUES ${placeholders}) AS v(elo, volatility, user_id, queue_id)
54+
WHERE qu.user_id = v.user_id::text AND qu.queue_id = v.queue_id::integer`,
55+
values,
56+
)
57+
}
58+
59+
async function benchmark(
60+
name: string,
61+
fn: () => Promise<void>,
62+
operations: number,
63+
iterations: number = 1,
64+
): Promise<BenchmarkResult> {
65+
const startMem = process.memoryUsage().heapUsed
66+
const durations: number[] = []
67+
68+
for (let i = 0; i < iterations; i++) {
69+
const start = performance.now()
70+
await fn()
71+
const end = performance.now()
72+
durations.push(end - start)
73+
}
74+
75+
const endMem = process.memoryUsage().heapUsed
76+
const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length
77+
78+
return {
79+
name,
80+
duration: avgDuration,
81+
operations,
82+
opsPerSecond: (operations / avgDuration) * 1000,
83+
memoryUsed: endMem - startMem,
84+
}
85+
}
86+
87+
async function getExistingPlayers(
88+
queueId: number,
89+
limit: number,
90+
): Promise<string[]> {
91+
const result = await pool.query(
92+
`SELECT user_id FROM queue_users WHERE queue_id = $1 LIMIT $2`,
93+
[queueId, limit],
94+
)
95+
return result.rows.map((row) => row.user_id)
96+
}
97+
98+
async function getQueueId(): Promise<number> {
99+
const result = await pool.query(`SELECT id FROM queues LIMIT 1`)
100+
if (result.rows.length === 0) {
101+
throw new Error('No queues found in database')
102+
}
103+
return result.rows[0].id
104+
}
105+
106+
async function runBenchmark(
107+
name: string,
108+
queueId: number,
109+
playerCount: number,
110+
method: 'individual' | 'bulk',
111+
iterations: number = 3,
112+
): Promise<BenchmarkResult> {
113+
console.log(`\nRunning: ${name}...`)
114+
115+
const userIds = await getExistingPlayers(queueId, playerCount)
116+
117+
if (userIds.length < playerCount) {
118+
console.log(`⚠ Only found ${userIds.length} players, using that instead`)
119+
}
120+
121+
const result = await benchmark(
122+
name,
123+
async () => {
124+
if (method === 'individual') {
125+
for (const userId of userIds) {
126+
await updatePlayerMmrIndividual(
127+
queueId,
128+
userId,
129+
1000 + Math.random() * 50,
130+
Math.floor(Math.random() * 10),
131+
)
132+
}
133+
} else {
134+
const updates = userIds.map((userId) => ({
135+
user_id: userId,
136+
elo: 1000 + Math.random() * 50,
137+
volatility: Math.floor(Math.random() * 10),
138+
}))
139+
await updatePlayerMmrBulk(queueId, updates)
140+
}
141+
},
142+
userIds.length,
143+
iterations,
144+
)
145+
146+
console.log(
147+
`✓ Completed: ${result.duration.toFixed(2)}ms (${result.opsPerSecond.toFixed(2)} ops/sec)`,
148+
)
149+
return result
150+
}
151+
152+
async function main() {
153+
console.log('=== MMR Update Benchmark ===\n')
154+
console.log('Warming up connection...\n')
155+
156+
await pool.query('SELECT 1')
157+
158+
const queueId = await getQueueId()
159+
console.log(`Using queue_id: ${queueId}`)
160+
161+
const totalPlayers = await pool.query(
162+
`SELECT COUNT(*) FROM queue_users WHERE queue_id = $1`,
163+
[queueId],
164+
)
165+
console.log(`Total players in queue: ${totalPlayers.rows[0].count}`)
166+
console.log('Iterations per test: 3\n')
167+
168+
const results: BenchmarkResult[] = []
169+
170+
const testCases = [
171+
{ players: 10, label: '10 players' },
172+
{ players: 50, label: '50 players' },
173+
{ players: 100, label: '100 players' },
174+
{ players: 500, label: '500 players' },
175+
{ players: 1000, label: '1000 players' },
176+
]
177+
178+
for (const testCase of testCases) {
179+
results.push(
180+
await runBenchmark(
181+
`${testCase.label} - individual`,
182+
queueId,
183+
testCase.players,
184+
'individual',
185+
),
186+
)
187+
188+
results.push(
189+
await runBenchmark(
190+
`${testCase.label} - bulk`,
191+
queueId,
192+
testCase.players,
193+
'bulk',
194+
),
195+
)
196+
}
197+
198+
console.log('\n\n=== Results ===\n')
199+
console.table(
200+
results.map((r) => ({
201+
Test: r.name,
202+
'Avg Duration (ms)': r.duration.toFixed(2),
203+
Ops: r.operations,
204+
'Ops/sec': r.opsPerSecond.toFixed(2),
205+
'Memory (KB)': (r.memoryUsed / 1024).toFixed(2),
206+
})),
207+
)
208+
209+
console.log('\n=== Performance Comparison ===\n')
210+
for (let i = 0; i < results.length; i += 2) {
211+
const individual = results[i]
212+
const bulk = results[i + 1]
213+
const speedup = individual.duration / bulk.duration
214+
215+
console.log(`${individual.name.split(' - ')[0]}:`)
216+
console.log(` Bulk is ${speedup.toFixed(2)}x faster`)
217+
console.log(
218+
` Time saved: ${(individual.duration - bulk.duration).toFixed(2)}ms`,
219+
)
220+
console.log(
221+
` Memory diff: ${((bulk.memoryUsed - individual.memoryUsed) / 1024).toFixed(2)}KB\n`,
222+
)
223+
}
224+
225+
await pool.end()
226+
console.log('Benchmark complete.')
227+
}
228+
229+
main().catch((error) => {
230+
console.error('Benchmark failed:', error)
231+
pool.end().finally(() => process.exit(1))
232+
})

src/utils/algorithms/calculateMMR.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
getQueueSettings,
33
getUsersNeedingRoleUpdates,
44
updatePlayerMmrAll,
5+
updatePlayerMmrBulk,
56
} from '../queryDB'
67
import type { Matches, Queues, teamResults } from 'psqlDB'
78
import { setUserQueueRole } from 'utils/queueHelpers'
@@ -143,36 +144,53 @@ export async function calculateNewMMR(
143144
queueSettings.default_elo,
144145
)
145146

146-
const playerMMRChanges: Array<{ user_id: string; oldMMR: number; newMMR: number }> = []
147+
const playerMMRChanges: Array<{
148+
user_id: string
149+
oldMMR: number
150+
newMMR: number
151+
}> = []
147152
const updatePromises: Promise<void>[] = []
148153

154+
const playerUpdates: Array<{
155+
user_id: string
156+
elo: number
157+
volatility: number
158+
}> = []
159+
149160
for (const ts of teamStats) {
150-
const isWinner = ts.isWinner
151-
const mmrChange = isWinner ? ratingChange : -ratingChange / loserCount
161+
const mmrChange = ts.isWinner ? ratingChange : -ratingChange / loserCount
162+
const mmrChangeRounded = Math.round(mmrChange * 10) / 10
152163

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

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

160172
playerMMRChanges.push({
161173
user_id: player.user_id,
162174
oldMMR,
163-
newMMR,
175+
newMMR: clampedMMR,
164176
})
165177

166-
player.elo = clamp(newMMR, 0, 9999)
167-
player.elo_change = parseFloat(mmrChange.toFixed(1))
178+
player.elo = clampedMMR
179+
player.elo_change = mmrChangeRounded
168180
player.volatility = newVolatility
169181

170-
updatePromises.push(
171-
updatePlayerMmrAll(queueId, player.user_id, newMMR, newVolatility),
172-
)
182+
playerUpdates.push({
183+
user_id: player.user_id,
184+
elo: clampedMMR,
185+
volatility: newVolatility,
186+
})
173187
}
174188

175-
ts.team.score = isWinner ? 1 : 0
189+
ts.team.score = ts.isWinner ? 1 : 0
190+
}
191+
192+
if (playerUpdates.length > 0) {
193+
await updatePlayerMmrBulk(queueId, playerUpdates)
176194
}
177195

178196
await Promise.all(updatePromises)

src/utils/queryDB.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,12 @@ export async function getUsersNeedingRoleUpdates(
317317
[queueId],
318318
)
319319

320-
const thresholds = roles.rows.map(r => r.mmr_threshold)
320+
const thresholds = roles.rows.map((r) => r.mmr_threshold)
321321
const usersToUpdate: string[] = []
322322

323323
for (const player of players) {
324-
const oldRole = thresholds.find(t => t <= player.oldMMR)
325-
const newRole = thresholds.find(t => t <= player.newMMR)
324+
const oldRole = thresholds.find((t) => t <= player.oldMMR)
325+
const newRole = thresholds.find((t) => t <= player.newMMR)
326326

327327
if (oldRole !== newRole) {
328328
usersToUpdate.push(player.user_id)
@@ -1084,6 +1084,35 @@ export async function updatePlayerMmrAll(
10841084
)
10851085
}
10861086

1087+
export async function updatePlayerMmrBulk(
1088+
queueId: number,
1089+
updates: Array<{ user_id: string; elo: number; volatility: number }>,
1090+
): Promise<void> {
1091+
const values = updates.flatMap((u) => [
1092+
u.elo,
1093+
u.volatility,
1094+
u.user_id,
1095+
queueId,
1096+
])
1097+
1098+
const placeholders = updates
1099+
.map((_, i) => {
1100+
const offset = i * 4
1101+
return `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4})`
1102+
})
1103+
.join(', ')
1104+
1105+
await pool.query(
1106+
`UPDATE queue_users AS qu
1107+
SET elo = v.elo::numeric,
1108+
peak_elo = GREATEST(qu.peak_elo, v.elo::numeric),
1109+
volatility = v.volatility::integer
1110+
FROM (VALUES ${placeholders}) AS v(elo, volatility, user_id, queue_id)
1111+
WHERE qu.user_id = v.user_id::text AND qu.queue_id = v.queue_id::integer`,
1112+
values,
1113+
)
1114+
}
1115+
10871116
export async function updatePlayerElo(
10881117
queueId: number,
10891118
userId: string,

0 commit comments

Comments
 (0)