Skip to content

Commit e5b85e8

Browse files
committed
fix: finalize tests
1 parent 68b9bee commit e5b85e8

4 files changed

Lines changed: 184 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Fluxel is intentionally in MVP mode.
5252
- No `log/status/list/checkout` CLI surface yet.
5353
- No `s5cmd` command-list generation path for bulk transfer yet.
5454
- S3 branch locking now recovers expired stale lock objects automatically, but there is still no operator-facing lock inspection or cleanup command.
55+
- S3 branch locking now recovers expired stale lock objects automatically, but there is still no operator-facing lock inspection or cleanup command.
5556

5657
## Technical Stack
5758

scripts/run_s3_integration.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,4 @@ wait_for_ministack
8888
curl -fsS -X POST "$RESET_URL" >/dev/null 2>&1 || true
8989

9090
cd "$ROOT_DIR"
91-
uv run pytest tests/test_s3_integration.py -m integration "$@"
91+
uv run pytest tests/test_s3_integration.py -v -s -m integration "$@"

src/fluxel/core/SPEC.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,49 @@
11
# Spec (Human Authored)
22

3+
4+
## `flx add`
5+
6+
We add files using two (2) different modes:
7+
8+
1. `meta` which is a pure metadata transaction, we simply store location of file with a hash of name + size
9+
2. `hash` which is a git-like transaction where we store the file as `hash[:2]/hash[2:]`
10+
- We're using `blake3` to calculate the hash
11+
12+
The action of adding also needs to store a Index.
13+
14+
We chose actively not to store the actions transpired (e.g. `mv`, `add`, `rm`, ..)
15+
16+
The `add` action is local until commited.
17+
18+
### Logic
19+
20+
O(1) and simply appends to a list of operations.
21+
22+
## `flx rm`
23+
24+
Removes, like `add` very much.
25+
26+
### Logic
27+
28+
O(1) simply appends to a list of operations.
29+
30+
31+
## `flx commit`
32+
33+
Commits and saves all the added (or removed/moved) files to a set branch.
34+
35+
## `flx branch`
36+
37+
Branch is checked out on S3-level directly.
38+
39+
## `flx merge`
40+
41+
Merge is done on S3-level.
42+
43+
## `flx diff`
44+
45+
Diff should be quite fast, but is allowed to use analytical index (that's built on-demand).
46+
47+
## `flx index`
48+
49+
Build an analytical index on-demand that's using DuckDB.

tests/test_s3_integration.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import time
34
from pathlib import Path
45
from uuid import uuid4
56

@@ -195,3 +196,137 @@ def test_s3_integration_reports_optimistic_concurrency_conflicts(
195196
assert error.operation == "commit"
196197
assert error.expected_commit_id == base_commit
197198
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

Comments
 (0)