|
| 1 | +import { createTestClient } from '@zenstackhq/testtools'; |
| 2 | +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; |
| 3 | + |
| 4 | +describe.skip('Memory usage test with repeated CRUD operations', () => { |
| 5 | + let client: any; |
| 6 | + |
| 7 | + beforeEach(async () => { |
| 8 | + client = await createTestClient( |
| 9 | + ` |
| 10 | +model User { |
| 11 | + id String @id @default(cuid()) |
| 12 | + email String @unique |
| 13 | + name String |
| 14 | + createdAt DateTime @default(now()) |
| 15 | + posts Post[] |
| 16 | + comments Comment[] |
| 17 | +} |
| 18 | +
|
| 19 | +model Post { |
| 20 | + id String @id @default(cuid()) |
| 21 | + title String |
| 22 | + content String |
| 23 | + published Boolean @default(false) |
| 24 | + createdAt DateTime @default(now()) |
| 25 | + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) |
| 26 | + authorId String |
| 27 | + comments Comment[] |
| 28 | +} |
| 29 | +
|
| 30 | +model Comment { |
| 31 | + id String @id @default(cuid()) |
| 32 | + content String |
| 33 | + createdAt DateTime @default(now()) |
| 34 | + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) |
| 35 | + postId String |
| 36 | + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) |
| 37 | + authorId String |
| 38 | +} |
| 39 | +`, |
| 40 | + ); |
| 41 | + }); |
| 42 | + |
| 43 | + afterEach(async () => { |
| 44 | + await client?.$disconnect(); |
| 45 | + }); |
| 46 | + |
| 47 | + it('repeatedly executes CRUD operations with random data and tracks memory', async () => { |
| 48 | + // ============ CONFIGURATION ============ |
| 49 | + // Adjust these values to test different workload scenarios |
| 50 | + const iterations = 100; // Number of complete CRUD cycles to execute |
| 51 | + const usersCount = 10; // Number of users to create per iteration |
| 52 | + const postsPerUser = 5; // Number of posts per user |
| 53 | + const commentsPerPost = 3; // Number of comments per post |
| 54 | + |
| 55 | + // Calculated totals |
| 56 | + const totalPosts = usersCount * postsPerUser; |
| 57 | + const totalComments = totalPosts * commentsPerPost; |
| 58 | + |
| 59 | + const memorySnapshots: Array<{ |
| 60 | + iteration: number; |
| 61 | + rss: number; |
| 62 | + heapTotal: number; |
| 63 | + heapUsed: number; |
| 64 | + external: number; |
| 65 | + }> = []; |
| 66 | + |
| 67 | + // Helper function to generate random string |
| 68 | + const randomString = (length: number) => { |
| 69 | + return Math.random() |
| 70 | + .toString(36) |
| 71 | + .substring(2, 2 + length); |
| 72 | + }; |
| 73 | + |
| 74 | + // Helper function to generate random content |
| 75 | + const randomContent = () => { |
| 76 | + const paragraphs = Math.floor(Math.random() * 5) + 1; |
| 77 | + return Array.from({ length: paragraphs }, () => randomString(100)).join('\n\n'); |
| 78 | + }; |
| 79 | + |
| 80 | + console.log(`\nStarting ${iterations} iterations of CRUD operations...\n`); |
| 81 | + |
| 82 | + for (let i = 0; i < iterations; i++) { |
| 83 | + // ============ CREATE ============ |
| 84 | + |
| 85 | + // Create users |
| 86 | + const users = await Promise.all( |
| 87 | + Array.from({ length: usersCount }, (_, idx) => |
| 88 | + client.user.create({ |
| 89 | + data: { |
| 90 | + email: `user${i}-${idx + 1}-${randomString(8)}@test.com`, |
| 91 | + name: `User ${i}-${idx + 1} ${randomString(10)}`, |
| 92 | + }, |
| 93 | + }), |
| 94 | + ), |
| 95 | + ); |
| 96 | + |
| 97 | + // Create posts per user |
| 98 | + const posts: any[] = []; |
| 99 | + for (const user of users) { |
| 100 | + for (let j = 0; j < postsPerUser; j++) { |
| 101 | + const post = await client.post.create({ |
| 102 | + data: { |
| 103 | + title: `Post ${i}-${j} - ${randomString(20)}`, |
| 104 | + content: randomContent(), |
| 105 | + published: Math.random() > 0.5, |
| 106 | + authorId: user.id, |
| 107 | + }, |
| 108 | + }); |
| 109 | + posts.push(post); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + // Create comments per post |
| 114 | + const comments: any[] = []; |
| 115 | + for (const post of posts) { |
| 116 | + for (let k = 0; k < commentsPerPost; k++) { |
| 117 | + const randomAuthor = users[Math.floor(Math.random() * users.length)]!; |
| 118 | + const comment = await client.comment.create({ |
| 119 | + data: { |
| 120 | + content: randomString(100), |
| 121 | + postId: post.id, |
| 122 | + authorId: randomAuthor.id, |
| 123 | + }, |
| 124 | + }); |
| 125 | + comments.push(comment); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + // ============ READ ============ |
| 130 | + |
| 131 | + // Read all users with posts and comments |
| 132 | + const allUsers = await client.user.findMany({ |
| 133 | + include: { |
| 134 | + posts: { |
| 135 | + include: { |
| 136 | + comments: true, |
| 137 | + }, |
| 138 | + }, |
| 139 | + comments: true, |
| 140 | + }, |
| 141 | + }); |
| 142 | + expect(allUsers).toHaveLength(usersCount); |
| 143 | + |
| 144 | + // Read all posts with filtering |
| 145 | + await client.post.findMany({ |
| 146 | + where: { |
| 147 | + published: true, |
| 148 | + }, |
| 149 | + include: { |
| 150 | + author: true, |
| 151 | + comments: true, |
| 152 | + }, |
| 153 | + }); |
| 154 | + |
| 155 | + // Read individual comments |
| 156 | + await client.comment.findMany({ |
| 157 | + include: { |
| 158 | + post: true, |
| 159 | + author: true, |
| 160 | + }, |
| 161 | + }); |
| 162 | + |
| 163 | + // Aggregate operations |
| 164 | + const userCount = await client.user.count(); |
| 165 | + const postCount = await client.post.count(); |
| 166 | + const commentCount = await client.comment.count(); |
| 167 | + |
| 168 | + expect(userCount).toBeGreaterThanOrEqual(usersCount); |
| 169 | + expect(postCount).toBeGreaterThanOrEqual(totalPosts); |
| 170 | + expect(commentCount).toBeGreaterThanOrEqual(totalComments); |
| 171 | + |
| 172 | + // ============ UPDATE ============ |
| 173 | + |
| 174 | + // Update random posts |
| 175 | + const postsToUpdate = posts.slice(0, 5); |
| 176 | + for (const post of postsToUpdate) { |
| 177 | + await client.post.update({ |
| 178 | + where: { id: post.id }, |
| 179 | + data: { |
| 180 | + title: `Updated - ${randomString(20)}`, |
| 181 | + content: randomContent(), |
| 182 | + }, |
| 183 | + }); |
| 184 | + } |
| 185 | + |
| 186 | + // Update random users |
| 187 | + const userToUpdate = users[0]!; |
| 188 | + await client.user.update({ |
| 189 | + where: { id: userToUpdate.id }, |
| 190 | + data: { |
| 191 | + name: `Updated User - ${randomString(10)}`, |
| 192 | + }, |
| 193 | + }); |
| 194 | + |
| 195 | + // Update many comments |
| 196 | + await client.comment.updateMany({ |
| 197 | + where: { |
| 198 | + postId: posts[0]!.id, |
| 199 | + }, |
| 200 | + data: { |
| 201 | + content: `Bulk updated - ${randomString(50)}`, |
| 202 | + }, |
| 203 | + }); |
| 204 | + |
| 205 | + // ============ DELETE (Cleanup) ============ |
| 206 | + |
| 207 | + // Delete all comments first (due to foreign key constraints) |
| 208 | + await client.comment.deleteMany({}); |
| 209 | + |
| 210 | + // Delete all posts |
| 211 | + await client.post.deleteMany({}); |
| 212 | + |
| 213 | + // Delete all users |
| 214 | + await client.user.deleteMany({}); |
| 215 | + |
| 216 | + // Verify cleanup |
| 217 | + const remainingUsers = await client.user.count(); |
| 218 | + const remainingPosts = await client.post.count(); |
| 219 | + const remainingComments = await client.comment.count(); |
| 220 | + |
| 221 | + expect(remainingUsers).toBe(0); |
| 222 | + expect(remainingPosts).toBe(0); |
| 223 | + expect(remainingComments).toBe(0); |
| 224 | + |
| 225 | + // ============ MEMORY SNAPSHOT ============ |
| 226 | + |
| 227 | + // Force garbage collection if available (run tests with --expose-gc flag) |
| 228 | + if (global.gc) { |
| 229 | + global.gc(); |
| 230 | + } |
| 231 | + |
| 232 | + const memUsage = process.memoryUsage(); |
| 233 | + memorySnapshots.push({ |
| 234 | + iteration: i + 1, |
| 235 | + rss: memUsage.rss, |
| 236 | + heapTotal: memUsage.heapTotal, |
| 237 | + heapUsed: memUsage.heapUsed, |
| 238 | + external: memUsage.external, |
| 239 | + }); |
| 240 | + |
| 241 | + // Log progress every 10 iterations |
| 242 | + if ((i + 1) % 10 === 0) { |
| 243 | + console.log(`Completed ${i + 1}/${iterations} iterations`); |
| 244 | + console.log( |
| 245 | + ` Memory: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB heap used, ${(memUsage.rss / 1024 / 1024).toFixed(2)} MB RSS`, |
| 246 | + ); |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + // ============ MEMORY ANALYSIS ============ |
| 251 | + |
| 252 | + console.log('\n=== Memory Usage Summary ===\n'); |
| 253 | + |
| 254 | + const firstSnapshot = memorySnapshots[0]!; |
| 255 | + const lastSnapshot = memorySnapshots[memorySnapshots.length - 1]!; |
| 256 | + const maxHeapUsed = Math.max(...memorySnapshots.map((s) => s.heapUsed)); |
| 257 | + const minHeapUsed = Math.min(...memorySnapshots.map((s) => s.heapUsed)); |
| 258 | + const avgHeapUsed = memorySnapshots.reduce((sum, s) => sum + s.heapUsed, 0) / memorySnapshots.length; |
| 259 | + |
| 260 | + const formatMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2); |
| 261 | + |
| 262 | + console.log('Heap Used:'); |
| 263 | + console.log(` Initial: ${formatMB(firstSnapshot.heapUsed)} MB`); |
| 264 | + console.log(` Final: ${formatMB(lastSnapshot.heapUsed)} MB`); |
| 265 | + console.log(` Min: ${formatMB(minHeapUsed)} MB`); |
| 266 | + console.log(` Max: ${formatMB(maxHeapUsed)} MB`); |
| 267 | + console.log(` Average: ${formatMB(avgHeapUsed)} MB`); |
| 268 | + console.log( |
| 269 | + ` Growth: ${formatMB(lastSnapshot.heapUsed - firstSnapshot.heapUsed)} MB (${(((lastSnapshot.heapUsed - firstSnapshot.heapUsed) / firstSnapshot.heapUsed) * 100).toFixed(2)}%)`, |
| 270 | + ); |
| 271 | + |
| 272 | + console.log('\nRSS (Resident Set Size):'); |
| 273 | + console.log(` Initial: ${formatMB(firstSnapshot.rss)} MB`); |
| 274 | + console.log(` Final: ${formatMB(lastSnapshot.rss)} MB`); |
| 275 | + console.log( |
| 276 | + ` Growth: ${formatMB(lastSnapshot.rss - firstSnapshot.rss)} MB (${(((lastSnapshot.rss - firstSnapshot.rss) / firstSnapshot.rss) * 100).toFixed(2)}%)`, |
| 277 | + ); |
| 278 | + |
| 279 | + console.log('\nHeap Total:'); |
| 280 | + console.log(` Initial: ${formatMB(firstSnapshot.heapTotal)} MB`); |
| 281 | + console.log(` Final: ${formatMB(lastSnapshot.heapTotal)} MB`); |
| 282 | + |
| 283 | + console.log('\n=== Test Summary ==='); |
| 284 | + console.log(`Total iterations: ${iterations}`); |
| 285 | + console.log(`Operations per iteration:`); |
| 286 | + console.log(` - Created: ${usersCount} users, ${totalPosts} posts, ${totalComments} comments`); |
| 287 | + console.log(` - Read: Multiple queries with includes and filters`); |
| 288 | + console.log(` - Updated: 5 posts, 1 user, bulk comment updates`); |
| 289 | + console.log(` - Deleted: All data (cleanup)`); |
| 290 | + const opsPerIteration = usersCount + totalPosts + totalComments + 10; // approximate CRUD ops |
| 291 | + console.log(`Total operations: ~${iterations * opsPerIteration}`); |
| 292 | + |
| 293 | + // Check for significant memory leaks (> 50% growth is concerning) |
| 294 | + const heapGrowthPercent = ((lastSnapshot.heapUsed - firstSnapshot.heapUsed) / firstSnapshot.heapUsed) * 100; |
| 295 | + if (heapGrowthPercent > 50) { |
| 296 | + console.log( |
| 297 | + `\n⚠️ Warning: Heap usage grew by ${heapGrowthPercent.toFixed(2)}% which may indicate a memory leak`, |
| 298 | + ); |
| 299 | + } else { |
| 300 | + console.log(`\n✓ Memory usage appears stable (${heapGrowthPercent.toFixed(2)}% growth)`); |
| 301 | + } |
| 302 | + |
| 303 | + console.log('\n'); |
| 304 | + |
| 305 | + // Store snapshots for potential further analysis |
| 306 | + expect(memorySnapshots).toHaveLength(iterations); |
| 307 | + }, 120000); // 2 minute timeout for the test |
| 308 | +}); |
0 commit comments