|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import time |
3 | 4 | from pathlib import Path |
4 | 5 | from uuid import uuid4 |
5 | 6 |
|
@@ -195,3 +196,137 @@ def test_s3_integration_reports_optimistic_concurrency_conflicts( |
195 | 196 | assert error.operation == "commit" |
196 | 197 | assert error.expected_commit_id == base_commit |
197 | 198 | assert error.current_commit_id == winning_commit |
| 199 | + |
| 200 | + |
| 201 | +def test_s3_integration_million_file_scale( |
| 202 | + tmp_path: Path, |
| 203 | + ministack_client, |
| 204 | + s3_repo_root: str, |
| 205 | + caplog, |
| 206 | +) -> None: |
| 207 | + """Test manifest index performance at 1M files with timing measurements. |
| 208 | +
|
| 209 | + Validates: |
| 210 | + - Specific file lookup is O(log B + 1) |
| 211 | + - Prefix listing with 200 matches is O(log B + 200) |
| 212 | + - Bulk downloads use manifest cache effectively |
| 213 | +
|
| 214 | + Logs timing measurements for performance regression detection. |
| 215 | + """ |
| 216 | + worktree = tmp_path / "worktree" |
| 217 | + client_root = tmp_path / "client-state" |
| 218 | + worktree.mkdir(parents=True) |
| 219 | + client_root.mkdir(parents=True) |
| 220 | + |
| 221 | + # 1. Generate 1M files locally (much faster than S3 API calls) |
| 222 | + print("\n[SCALE TEST] Generating 1M files locally...") |
| 223 | + gen_start = time.perf_counter() |
| 224 | + (worktree / "images" / "cats").mkdir(parents=True, exist_ok=True) |
| 225 | + (worktree / "images" / "dogs").mkdir(parents=True, exist_ok=True) |
| 226 | + (worktree / "logs").mkdir(parents=True, exist_ok=True) |
| 227 | + (worktree / "other").mkdir(parents=True, exist_ok=True) |
| 228 | + cat_content = f"cat".encode() |
| 229 | + dog_content = f"dog".encode() |
| 230 | + data_content = f"data".encode() |
| 231 | + |
| 232 | + for i in range(1_000_000): |
| 233 | + # Distribute: images/cats/* (200), images/dogs/* (300), other/* (999_500) |
| 234 | + if i < 200: |
| 235 | + path = worktree / "images" / "cats" / f"cat_{i:06d}.jpg" |
| 236 | + # content = cat_content |
| 237 | + elif i < 500: |
| 238 | + path = worktree / "images" / "dogs" / f"dog_{i:06d}.jpg" |
| 239 | + # content = dog_content |
| 240 | + else: |
| 241 | + # Alternate between logs and other for realistic distribution |
| 242 | + category = "logs" if (i % 2) == 0 else "other" |
| 243 | + path = worktree / category / f"file_{i:07d}.bin" |
| 244 | + # content = data_content |
| 245 | + |
| 246 | + path.touch() |
| 247 | + |
| 248 | + gen_time = time.perf_counter() - gen_start |
| 249 | + print( |
| 250 | + f"[SCALE TEST] Generated 1M files in {gen_time:.2f}s ({1_000_000/gen_time:.0f} files/sec)" |
| 251 | + ) |
| 252 | + |
| 253 | + # 2. Commit to Fluxel (creates manifest + index from local files) |
| 254 | + print("[SCALE TEST] Committing 1M files to Fluxel...") |
| 255 | + repo = _open_remote_repo( |
| 256 | + s3_repo_root, |
| 257 | + worktree=worktree, |
| 258 | + client_root=client_root, |
| 259 | + s3_client=ministack_client, |
| 260 | + ) |
| 261 | + commit_start = time.perf_counter() |
| 262 | + commit_id = repo.commit("1M file snapshot (meta)", identity_mode="meta") |
| 263 | + commit_time = time.perf_counter() - commit_start |
| 264 | + assert commit_id |
| 265 | + print( |
| 266 | + f"[SCALE TEST] Commit + manifest build in {commit_time:.2f}s ({1_000_000/commit_time:.0f} files/sec)" |
| 267 | + ) |
| 268 | + |
| 269 | + # 3. Test: Specific file lookup (should be O(log B + 1)) |
| 270 | + print("[SCALE TEST] Testing specific file lookup...") |
| 271 | + lookup_start = time.perf_counter() |
| 272 | + cat_50 = repo.resolve_entry("main", "images/cats/cat_000050.jpg") |
| 273 | + lookup_time = time.perf_counter() - lookup_start |
| 274 | + assert cat_50 is not None |
| 275 | + assert cat_50.size == 0 # touch |
| 276 | + print(f"[SCALE TEST] Single file lookup: {lookup_time*1000:.3f}ms") |
| 277 | + |
| 278 | + # 4. Test: Prefix listing (should be O(log B + 200)) |
| 279 | + print("[SCALE TEST] Testing prefix listing (images/cats/*)...") |
| 280 | + listing_start = time.perf_counter() |
| 281 | + cats = repo.resolve_entries_for_prefix("main", "images/cats") |
| 282 | + listing_time = time.perf_counter() - listing_start |
| 283 | + assert len(cats) == 200 |
| 284 | + print(f"[SCALE TEST] Prefix listing 200 files: {listing_time*1000:.3f}ms") |
| 285 | + |
| 286 | + # 5. Test: Bulk metadata access (simulating download planning) |
| 287 | + print("[SCALE TEST] Bulk metadata access (200 files)...") |
| 288 | + bulk_start = time.perf_counter() |
| 289 | + bulk_data = [] |
| 290 | + for path, entry in cats.items(): |
| 291 | + # Simulate metadata-only access (no blob reads) |
| 292 | + bulk_data.append((path, entry.size, entry.identity_mode)) |
| 293 | + bulk_time = time.perf_counter() - bulk_start |
| 294 | + assert len(bulk_data) == 200 |
| 295 | + assert all(t[1] == 0 for t in bulk_data) # All have size |
| 296 | + print(f"[SCALE TEST] Bulk metadata access (200): {bulk_time*1000:.3f}ms") |
| 297 | + |
| 298 | + # 6. Test: Cached prefix listing (should be faster) |
| 299 | + print("[SCALE TEST] Testing cached prefix listing...") |
| 300 | + cached_start = time.perf_counter() |
| 301 | + cats_again = repo.resolve_entries_for_prefix("main", "images/cats") |
| 302 | + cached_time = time.perf_counter() - cached_start |
| 303 | + assert len(cats_again) == 200 |
| 304 | + print(f"[SCALE TEST] Cached prefix listing: {cached_time*1000:.3f}ms") |
| 305 | + |
| 306 | + # 7. Summary and assertions |
| 307 | + print("\n[SCALE TEST] Performance Summary:") |
| 308 | + print( |
| 309 | + f" Generate 1M files: {gen_time:.2f}s ({1_000_000/gen_time:.0f} files/sec)" |
| 310 | + ) |
| 311 | + print( |
| 312 | + f" Commit + manifest: {commit_time:.2f}s ({1_000_000/commit_time:.0f} files/sec)" |
| 313 | + ) |
| 314 | + print(f" Single lookup: {lookup_time*1000:.3f}ms") |
| 315 | + print(f" Prefix list (200): {listing_time*1000:.3f}ms") |
| 316 | + print(f" Bulk metadata (200): {bulk_time*1000:.3f}ms") |
| 317 | + print(f" Cached prefix list: {cached_time*1000:.3f}ms") |
| 318 | + |
| 319 | + # Verify manifest index is working: specific lookup should be fast (< 100ms) |
| 320 | + assert ( |
| 321 | + lookup_time < 0.1 |
| 322 | + ), f"Single lookup took {lookup_time*1000:.3f}ms (expected < 100ms)" |
| 323 | + |
| 324 | + # Prefix listing should also be fast (< 200ms for 200 matches) |
| 325 | + assert ( |
| 326 | + listing_time < 0.2 |
| 327 | + ), f"Prefix listing took {listing_time*1000:.3f}ms (expected < 200ms)" |
| 328 | + |
| 329 | + # Cached listing should be noticeably faster than initial |
| 330 | + assert ( |
| 331 | + cached_time <= listing_time or cached_time < 0.1 |
| 332 | + ), "Cached listing should be <= initial listing time" |
0 commit comments