ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(api): add upload performance test

byarielm.fyi bdd53d59 d0dd17ae

verified
+339
+339
packages/api/__tests__/performance/upload.perf.test.ts
··· 1 + /** 2 + * Upload & Results Performance Tests 3 + * 4 + * Validates the save endpoint and uploads query performance with large datasets. 5 + * These tests use real database operations to measure DB throughput. 6 + */ 7 + 8 + import { describe, it, expect, beforeAll, afterAll } from 'vitest'; 9 + import { authRequest, requestWithSession, parseResponse, testId } from '../helpers'; 10 + import { 11 + createTestSession, 12 + cleanupAllTestSessions, 13 + cleanupAllTestData, 14 + } from '../fixtures'; 15 + import { UploadRepository } from '../../src/repositories/UploadRepository'; 16 + 17 + describe('Upload Performance Tests', { timeout: 60000 }, () => { 18 + let validSession: string; 19 + const testUploadIds: string[] = []; 20 + 21 + beforeAll(async () => { 22 + validSession = await createTestSession('power'); 23 + }); 24 + 25 + afterAll(async () => { 26 + await cleanupAllTestData(); 27 + }); 28 + 29 + describe('Save Results Throughput', () => { 30 + it('saves 500 results with matches under 30s', async () => { 31 + const uploadId = testId('perf-save-500'); 32 + testUploadIds.push(uploadId); 33 + 34 + // Generate 500 results — 300 with matches, 200 without 35 + const results = Array.from({ length: 500 }, (_, i) => { 36 + const hasMatch = i < 300; 37 + return { 38 + sourceUser: { 39 + username: `perf_save_user_${String(i).padStart(4, '0')}`, 40 + date: '2026-01-15', 41 + }, 42 + atprotoMatches: hasMatch 43 + ? [ 44 + { 45 + did: `did:plc:perf-save-match-${String(i).padStart(4, '0')}`, 46 + handle: `perfmatch${i}.bsky.social`, 47 + displayName: `Perf Match ${i}`, 48 + matchScore: 80 + (i % 20), 49 + postCount: 10 + i, 50 + followerCount: 100 + i * 2, 51 + }, 52 + ] 53 + : [], 54 + isSearching: false, 55 + }; 56 + }); 57 + 58 + const start = performance.now(); 59 + const res = await requestWithSession( 60 + '/api/results/save', 61 + validSession, 62 + { 63 + method: 'POST', 64 + body: JSON.stringify({ 65 + uploadId, 66 + sourcePlatform: 'performance-test', 67 + results, 68 + }), 69 + }, 70 + ); 71 + const duration = performance.now() - start; 72 + 73 + expect(res.status).toBe(200); 74 + 75 + const body = await parseResponse<{ 76 + success: boolean; 77 + uploadId: string; 78 + totalUsers: number; 79 + matchedUsers: number; 80 + unmatchedUsers: number; 81 + }>(res); 82 + 83 + expect(body.success).toBe(true); 84 + expect(body.uploadId).toBe(uploadId); 85 + expect(body.totalUsers).toBe(500); 86 + expect(body.matchedUsers).toBe(300); 87 + expect(body.unmatchedUsers).toBe(200); 88 + 89 + // Performance assertion: 500 results should save under 30s 90 + expect(duration).toBeLessThan(30000); 91 + console.log( 92 + `[Perf] 500-result save completed in ${duration.toFixed(0)}ms ` + 93 + `(${body.matchedUsers} matched, ${body.unmatchedUsers} unmatched)`, 94 + ); 95 + }); 96 + 97 + it('saves 1000 results under 30s', async () => { 98 + const uploadId = testId('perf-save-1000'); 99 + testUploadIds.push(uploadId); 100 + 101 + // Generate 1000 results — 600 with matches, 400 without 102 + const results = Array.from({ length: 1000 }, (_, i) => { 103 + const hasMatch = i < 600; 104 + return { 105 + sourceUser: { 106 + username: `perf_save_1k_user_${String(i).padStart(4, '0')}`, 107 + date: '2026-01-20', 108 + }, 109 + atprotoMatches: hasMatch 110 + ? [ 111 + { 112 + did: `did:plc:perf-save-1k-match-${String(i).padStart(4, '0')}`, 113 + handle: `perf1kmatch${i}.bsky.social`, 114 + displayName: `Perf 1K Match ${i}`, 115 + matchScore: 70 + (i % 30), 116 + postCount: 5 + i, 117 + followerCount: 50 + i, 118 + }, 119 + ] 120 + : [], 121 + isSearching: false, 122 + }; 123 + }); 124 + 125 + const start = performance.now(); 126 + const res = await requestWithSession( 127 + '/api/results/save', 128 + validSession, 129 + { 130 + method: 'POST', 131 + body: JSON.stringify({ 132 + uploadId, 133 + sourcePlatform: 'performance-test', 134 + results, 135 + }), 136 + }, 137 + ); 138 + const duration = performance.now() - start; 139 + 140 + expect(res.status).toBe(200); 141 + 142 + const body = await parseResponse<{ 143 + success: boolean; 144 + totalUsers: number; 145 + matchedUsers: number; 146 + unmatchedUsers: number; 147 + }>(res); 148 + 149 + expect(body.success).toBe(true); 150 + expect(body.totalUsers).toBe(1000); 151 + expect(body.matchedUsers).toBe(600); 152 + expect(body.unmatchedUsers).toBe(400); 153 + 154 + // Performance assertion: 1000 results should save under 30s 155 + expect(duration).toBeLessThan(30000); 156 + console.log( 157 + `[Perf] 1000-result save completed in ${duration.toFixed(0)}ms ` + 158 + `(${body.matchedUsers} matched, ${body.unmatchedUsers} unmatched)`, 159 + ); 160 + }); 161 + 162 + it('saves results with multiple matches per user', async () => { 163 + const uploadId = testId('perf-save-multi'); 164 + testUploadIds.push(uploadId); 165 + 166 + // 200 results, each with 3-5 AT Protocol matches 167 + const results = Array.from({ length: 200 }, (_, i) => ({ 168 + sourceUser: { 169 + username: `perf_multi_user_${String(i).padStart(3, '0')}`, 170 + date: '2026-02-01', 171 + }, 172 + atprotoMatches: Array.from( 173 + { length: 3 + (i % 3) }, // 3, 4, or 5 matches per user 174 + (_, j) => ({ 175 + did: `did:plc:perf-multi-${i}-match-${j}`, 176 + handle: `perfmulti${i}m${j}.bsky.social`, 177 + displayName: `Multi Match ${i}-${j}`, 178 + matchScore: 90 - j * 10, 179 + postCount: 20 + j, 180 + followerCount: 200 + j * 50, 181 + }), 182 + ), 183 + isSearching: false, 184 + })); 185 + 186 + const totalMatches = results.reduce( 187 + (sum, r) => sum + r.atprotoMatches.length, 188 + 0, 189 + ); 190 + 191 + const start = performance.now(); 192 + const res = await requestWithSession( 193 + '/api/results/save', 194 + validSession, 195 + { 196 + method: 'POST', 197 + body: JSON.stringify({ 198 + uploadId, 199 + sourcePlatform: 'performance-test', 200 + results, 201 + }), 202 + }, 203 + ); 204 + const duration = performance.now() - start; 205 + 206 + expect(res.status).toBe(200); 207 + 208 + const body = await parseResponse<{ 209 + success: boolean; 210 + totalUsers: number; 211 + matchedUsers: number; 212 + }>(res); 213 + 214 + expect(body.success).toBe(true); 215 + expect(body.totalUsers).toBe(200); 216 + expect(body.matchedUsers).toBe(200); // All have matches 217 + 218 + // With multiple matches per user, total match records is higher 219 + // The save should still complete within 30s 220 + expect(duration).toBeLessThan(30000); 221 + console.log( 222 + `[Perf] 200-user multi-match save (${totalMatches} total matches) ` + 223 + `completed in ${duration.toFixed(0)}ms`, 224 + ); 225 + }); 226 + }); 227 + 228 + describe('Database Query Performance', () => { 229 + it('queries uploads list with 100+ entries efficiently', async () => { 230 + const uploadRepo = new UploadRepository(); 231 + const userDid = 'did:plc:test-power-user-002'; 232 + 233 + // Create 100 uploads directly via repository for speed 234 + const createPromises = Array.from({ length: 100 }, (_, i) => { 235 + const id = testId(`perf-query-${String(i).padStart(3, '0')}`); 236 + testUploadIds.push(id); 237 + return uploadRepo.createUpload( 238 + id, 239 + userDid, 240 + i % 3 === 0 ? 'instagram' : i % 3 === 1 ? 'tiktok' : 'twitter', 241 + 50 + i, 242 + 20 + (i % 30), 243 + ); 244 + }); 245 + await Promise.all(createPromises); 246 + 247 + // Now query the uploads list via the API 248 + const start = performance.now(); 249 + const res = await requestWithSession( 250 + '/api/results/uploads', 251 + validSession, 252 + ); 253 + const duration = performance.now() - start; 254 + 255 + expect(res.status).toBe(200); 256 + 257 + const body = await parseResponse<{ 258 + success: boolean; 259 + data: { 260 + uploads: Array<{ 261 + uploadId: string; 262 + sourcePlatform: string; 263 + totalUsers: number; 264 + matchedUsers: number; 265 + }>; 266 + }; 267 + }>(res); 268 + 269 + expect(body.success).toBe(true); 270 + // Should have at least 100 uploads (plus any from earlier tests) 271 + expect(body.data.uploads.length).toBeGreaterThanOrEqual(100); 272 + 273 + // Verify uploads are sorted by created_at desc 274 + for (let i = 1; i < body.data.uploads.length; i++) { 275 + // All uploads should have valid data 276 + expect(body.data.uploads[i].uploadId).toBeTruthy(); 277 + expect(body.data.uploads[i].sourcePlatform).toBeTruthy(); 278 + } 279 + 280 + // Query should complete quickly even with 100+ entries 281 + expect(duration).toBeLessThan(5000); 282 + console.log( 283 + `[Perf] Uploads list query (${body.data.uploads.length} entries) ` + 284 + `completed in ${duration.toFixed(0)}ms`, 285 + ); 286 + }); 287 + 288 + it('retrieves upload details with pagination efficiently', async () => { 289 + // Use one of the uploads we created in the save tests (has actual results) 290 + // Find a save-500 or save-1000 upload 291 + const savUploadId = testUploadIds.find((id) => id.includes('perf-save-500')); 292 + 293 + if (!savUploadId) { 294 + // If save tests haven't run yet, skip 295 + console.log('[Perf] Skipping upload details test - no save upload found'); 296 + return; 297 + } 298 + 299 + const start = performance.now(); 300 + const res = await requestWithSession( 301 + `/api/results/upload-details?uploadId=${savUploadId}&page=1&pageSize=50`, 302 + validSession, 303 + ); 304 + const duration = performance.now() - start; 305 + 306 + expect(res.status).toBe(200); 307 + 308 + const body = await parseResponse<{ 309 + success: boolean; 310 + data: { 311 + results: Array<{ 312 + sourceUser: { username: string }; 313 + atprotoMatches: Array<{ did: string }>; 314 + }>; 315 + pagination: { 316 + page: number; 317 + pageSize: number; 318 + totalPages: number; 319 + totalUsers: number; 320 + hasNextPage: boolean; 321 + }; 322 + }; 323 + }>(res); 324 + 325 + expect(body.success).toBe(true); 326 + expect(body.data.results.length).toBeGreaterThan(0); 327 + expect(body.data.pagination.totalUsers).toBe(500); 328 + expect(body.data.pagination.hasNextPage).toBe(true); 329 + 330 + // Paginated query should be fast 331 + expect(duration).toBeLessThan(5000); 332 + console.log( 333 + `[Perf] Upload details query (page 1 of ${body.data.pagination.totalPages}, ` + 334 + `${body.data.pagination.totalUsers} total users) ` + 335 + `completed in ${duration.toFixed(0)}ms`, 336 + ); 337 + }); 338 + }); 339 + });