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 follow performance test

byarielm.fyi d0dd17ae d0a66cc7

verified
+315
+315
packages/api/__tests__/performance/follow.perf.test.ts
··· 1 + /** 2 + * Follow Performance Tests 3 + * 4 + * Validates batch follow endpoint performance using mock AT Protocol agents. 5 + * Tests the chunked concurrency model (5 parallel) and overall throughput. 6 + */ 7 + 8 + import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; 9 + import { requestWithSession, parseResponse } from '../helpers'; 10 + import { 11 + createTestSession, 12 + cleanupAllTestSessions, 13 + createMockAgent, 14 + createFollowAgent, 15 + } from '../fixtures'; 16 + import type { MockFollowRecord, MockProfile } from '../fixtures'; 17 + import { SessionService } from '../../src/services/SessionService'; 18 + 19 + describe('Follow Performance Tests', () => { 20 + let validSession: string; 21 + 22 + beforeAll(async () => { 23 + validSession = await createTestSession('power'); 24 + }); 25 + 26 + afterAll(async () => { 27 + await cleanupAllTestSessions(); 28 + }); 29 + 30 + describe('Batch Follow Throughput', () => { 31 + it('batch follow handles 100 DIDs under 10s', async () => { 32 + // Mock agent that succeeds on all follows 33 + const mockAgent = createFollowAgent(); 34 + 35 + const originalMethod = SessionService.getAgentForSession; 36 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 37 + agent: mockAgent, 38 + did: 'did:plc:test-power-user-002', 39 + client: {}, 40 + }); 41 + 42 + try { 43 + const dids = Array.from( 44 + { length: 100 }, 45 + (_, i) => `did:plc:perf-follow-target-${String(i).padStart(3, '0')}`, 46 + ); 47 + 48 + const start = performance.now(); 49 + const res = await requestWithSession( 50 + '/api/follow/batch-follow-users', 51 + validSession, 52 + { 53 + method: 'POST', 54 + body: JSON.stringify({ dids }), 55 + }, 56 + ); 57 + const duration = performance.now() - start; 58 + 59 + expect(res.status).toBe(200); 60 + 61 + const body = await parseResponse<{ 62 + success: boolean; 63 + data: { 64 + total: number; 65 + succeeded: number; 66 + failed: number; 67 + alreadyFollowing: number; 68 + results: Array<{ 69 + did: string; 70 + success: boolean; 71 + alreadyFollowing: boolean; 72 + error: string | null; 73 + }>; 74 + }; 75 + }>(res); 76 + 77 + expect(body.success).toBe(true); 78 + expect(body.data.total).toBe(100); 79 + expect(body.data.succeeded).toBe(100); 80 + expect(body.data.failed).toBe(0); 81 + expect(body.data.results).toHaveLength(100); 82 + 83 + // All results should be successful 84 + for (const result of body.data.results) { 85 + expect(result.success).toBe(true); 86 + expect(result.error).toBeNull(); 87 + } 88 + 89 + // Performance assertion: 100 follows should complete under 10s 90 + expect(duration).toBeLessThan(10000); 91 + console.log( 92 + `[Perf] 100-DID batch follow completed in ${duration.toFixed(0)}ms`, 93 + ); 94 + } finally { 95 + SessionService.getAgentForSession = originalMethod; 96 + } 97 + }); 98 + 99 + it('batch follow with mixed results maintains throughput', async () => { 100 + // Agent where some DIDs are already followed, some succeed, some fail 101 + const alreadyFollowing = Array.from( 102 + { length: 20 }, 103 + (_, i) => `did:plc:perf-mixed-already-${i}`, 104 + ); 105 + const failDids = Array.from( 106 + { length: 10 }, 107 + (_, i) => `did:plc:perf-mixed-fail-${i}`, 108 + ); 109 + 110 + const mockAgent = createFollowAgent({ alreadyFollowing, failDids }); 111 + 112 + const originalMethod = SessionService.getAgentForSession; 113 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 114 + agent: mockAgent, 115 + did: 'did:plc:test-power-user-002', 116 + client: {}, 117 + }); 118 + 119 + try { 120 + // Mix of already-following, new follows, and failures 121 + const dids = [ 122 + ...alreadyFollowing, 123 + ...failDids, 124 + ...Array.from( 125 + { length: 70 }, 126 + (_, i) => `did:plc:perf-mixed-new-${i}`, 127 + ), 128 + ]; 129 + 130 + const start = performance.now(); 131 + const res = await requestWithSession( 132 + '/api/follow/batch-follow-users', 133 + validSession, 134 + { 135 + method: 'POST', 136 + body: JSON.stringify({ dids }), 137 + }, 138 + ); 139 + const duration = performance.now() - start; 140 + 141 + expect(res.status).toBe(200); 142 + 143 + const body = await parseResponse<{ 144 + success: boolean; 145 + data: { 146 + total: number; 147 + succeeded: number; 148 + failed: number; 149 + alreadyFollowing: number; 150 + results: Array<{ 151 + did: string; 152 + success: boolean; 153 + alreadyFollowing: boolean; 154 + error: string | null; 155 + }>; 156 + }; 157 + }>(res); 158 + 159 + expect(body.success).toBe(true); 160 + expect(body.data.total).toBe(100); 161 + expect(body.data.succeeded).toBe(90); // 20 already + 70 new 162 + expect(body.data.failed).toBe(10); 163 + expect(body.data.alreadyFollowing).toBe(20); 164 + 165 + // Mixed results should still complete within 10s 166 + expect(duration).toBeLessThan(10000); 167 + console.log( 168 + `[Perf] 100-DID mixed batch follow completed in ${duration.toFixed(0)}ms ` + 169 + `(${body.data.alreadyFollowing} already, ${body.data.failed} failed, ` + 170 + `${body.data.succeeded - body.data.alreadyFollowing} new)`, 171 + ); 172 + } finally { 173 + SessionService.getAgentForSession = originalMethod; 174 + } 175 + }); 176 + 177 + it('chunked concurrency processes all chunks correctly', async () => { 178 + // Verify that the chunked concurrency model (5 parallel) processes all DIDs 179 + const processedDids: string[] = []; 180 + const mockAgent = createMockAgent({ 181 + listRecords: async () => ({ 182 + data: { records: [] as MockFollowRecord[], cursor: undefined }, 183 + }), 184 + createRecord: async (params) => { 185 + const targetDid = 186 + (params.record as { subject?: string }).subject ?? ''; 187 + processedDids.push(targetDid); 188 + return { 189 + uri: `at://did:plc:mock/app.bsky.graph.follow/${Date.now()}`, 190 + cid: 'mock-cid', 191 + }; 192 + }, 193 + }); 194 + 195 + const originalMethod = SessionService.getAgentForSession; 196 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 197 + agent: mockAgent, 198 + did: 'did:plc:test-power-user-002', 199 + client: {}, 200 + }); 201 + 202 + try { 203 + // 100 DIDs = 20 chunks of 5 204 + const dids = Array.from( 205 + { length: 100 }, 206 + (_, i) => `did:plc:chunk-test-${String(i).padStart(3, '0')}`, 207 + ); 208 + 209 + const res = await requestWithSession( 210 + '/api/follow/batch-follow-users', 211 + validSession, 212 + { 213 + method: 'POST', 214 + body: JSON.stringify({ dids }), 215 + }, 216 + ); 217 + 218 + expect(res.status).toBe(200); 219 + 220 + const body = await parseResponse<{ 221 + success: boolean; 222 + data: { total: number; succeeded: number }; 223 + }>(res); 224 + 225 + expect(body.data.total).toBe(100); 226 + expect(body.data.succeeded).toBe(100); 227 + 228 + // All 100 DIDs should have been processed via createRecord 229 + expect(processedDids).toHaveLength(100); 230 + 231 + // Verify all original DIDs were processed (order may vary due to concurrency) 232 + const processedSet = new Set(processedDids); 233 + for (const did of dids) { 234 + expect(processedSet.has(did)).toBe(true); 235 + } 236 + } finally { 237 + SessionService.getAgentForSession = originalMethod; 238 + } 239 + }); 240 + }); 241 + 242 + describe('Check Follow Status Performance', () => { 243 + it('check-status handles 100 DIDs efficiently', async () => { 244 + // Create mock that simulates paginated listRecords 245 + const followedDids = Array.from( 246 + { length: 50 }, 247 + (_, i) => `did:plc:perf-status-${i}`, 248 + ); 249 + 250 + const mockAgent = createMockAgent({ 251 + listRecords: async () => ({ 252 + data: { 253 + records: followedDids.map((did): MockFollowRecord => ({ 254 + uri: `at://did:plc:mock/app.bsky.graph.follow/${did}`, 255 + cid: 'mock-cid', 256 + value: { subject: did, createdAt: new Date().toISOString() }, 257 + })), 258 + cursor: undefined, 259 + }, 260 + }), 261 + }); 262 + 263 + const originalMethod = SessionService.getAgentForSession; 264 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 265 + agent: mockAgent, 266 + did: 'did:plc:test-power-user-002', 267 + client: {}, 268 + }); 269 + 270 + try { 271 + const dids = Array.from( 272 + { length: 100 }, 273 + (_, i) => `did:plc:perf-status-${i}`, 274 + ); 275 + 276 + const start = performance.now(); 277 + const res = await requestWithSession( 278 + '/api/follow/check-status', 279 + validSession, 280 + { 281 + method: 'POST', 282 + body: JSON.stringify({ dids }), 283 + }, 284 + ); 285 + const duration = performance.now() - start; 286 + 287 + expect(res.status).toBe(200); 288 + 289 + const body = await parseResponse<{ 290 + success: boolean; 291 + data: { followStatus: Record<string, boolean> }; 292 + }>(res); 293 + 294 + expect(body.success).toBe(true); 295 + expect(Object.keys(body.data.followStatus)).toHaveLength(100); 296 + 297 + // First 50 should be following, rest should not 298 + for (let i = 0; i < 50; i++) { 299 + expect(body.data.followStatus[`did:plc:perf-status-${i}`]).toBe(true); 300 + } 301 + for (let i = 50; i < 100; i++) { 302 + expect(body.data.followStatus[`did:plc:perf-status-${i}`]).toBe(false); 303 + } 304 + 305 + // Status check should complete quickly with mock agent 306 + expect(duration).toBeLessThan(5000); 307 + console.log( 308 + `[Perf] 100-DID follow status check completed in ${duration.toFixed(0)}ms`, 309 + ); 310 + } finally { 311 + SessionService.getAgentForSession = originalMethod; 312 + } 313 + }); 314 + }); 315 + });