A high-performance PyQt6 desktop library management application for organizing student study materials with intelligent in-memory search, file import, and task management. Port from JavaFX with enhanced architecture and professional optimization.
-
Library Management (CRUD)
- Add, edit, delete study items (notes, PDFs, media files)
- Metadata tagging and categorization
- Undo/redo stack for user actions
-
Intelligent File Import
- Recursive directory scanning with master-worker parallelism
- Support for
.txt,.md,.pdf,.docx,.pptx,.mp3,.mp4 - Optimized with Win32 API (
FindFirstFile/FindNextFile) on Windows - Deduplication via persistent
processed_pathsset - ~16-17 seconds for 50,000 files (linear O(N) scaling)
-
Search & Indexing
- In-memory inverted keyword index for sub-millisecond search
- Multi-word AND query with substring matching
- Tag frequency analysis
- Search latency: 0.01-0.54ms even at 50,000 items (vs Explorer's 50-500ms)
- Persisted index in binary format (pickle)
-
Task Management
- Priority queue for tracked tasks
- Heap-based ordering (O(log N) insert/pop)
- Persistence across sessions
-
Data Persistence
- Binary serialization with custom format (magic number + version validation)
- Atomic write via temp file with cleanup on failure
- Load/save cycle preserves all metadata and indexes
Views (PyQt6)
↓
Controllers (MVC orchestration)
↓
Services (Business logic)
├─ LibraryService (CRUD, undo/redo)
├─ SearchService (Inverted index, queries)
└─ ImportService (Directory scanning, Item creation)
↓
Models (Data objects)
├─ Item (file metadata)
├─ Task (priority queue)
└─ LibraryData (persistence envelope)
↓
Repository (Binary I/O)
└─ LibraryRepository (pickle with validation)
- Replaces: Top-level-subdirs-only ThreadPoolExecutor distribution
- New: Full-tree work-queue pattern using
threading.Queue - How: Any worker pops a directory from queue, scans it, enqueues discovered subdirs for other workers
- Benefit: Eliminates timeout overhead (~1s latency per worker), enables true parallel tree traversal
- Code: src/studyvault/utils/file_util.py -
scan_directory_async()
- Replaces:
pathlib.iterdir()→Path.is_file()→ extraGetFileAttributesWsyscall per entry - New:
win32file.FindFilesIterator()returnsWIN32_FIND_DATAstruct with attributes pre-cached - Savings: Eliminates N extra filesystem syscalls per directory scanned
- Benefit: ~20-40% faster on large trees (50K+ files)
- Fallback: Gracefully reverts to
pathlib.iterdir()on non-Windows or ifpywin32unavailable - Code: src/studyvault/utils/file_util.py
- Structure:
{keyword: Set[item_ids]}+{tag: frequency_count} - Complexity: O(T + M log M) where T = terms, M = matches (vs naive O(N*M))
- Latency: 0.01-0.54ms for 50K items
- Tradeoff: Costs ~2.4 KB per item in RAM (indexes included)
- Code: src/studyvault/services/search_service.py -
search()
Test Configuration:
- Platform: Windows 11
- Profile: "small" (1-4KB text, 40-80KB PDFs, 150-300KB audio)
- Repetitions: 5 per scale
- Import Mode: Parallel (master-worker + Win32 API)
- Memory Tracking: Enabled (
tracemalloc)
| Scale | Import (ms) | Index (ms) | Search (μs) | Peak Memory (MB) |
|---|---|---|---|---|
| 100 | 51 ± 24 | 0.4 ± 0.1 | 10.0 | 0.42 |
| 1K | 306 ± 19 | 2.5 ± 0.1 | 11.4 | 2.81 |
| 5K | 1,666 ± 141 | 19.6 ± 4.4 | 85.7 | 11.96 |
| 10K | 3,234 ± 122 | 25.0 ± 2.3 | 92.0 | 24.14 |
| 20K | 6,037 ± 58 | 51.2 ± 1.7 | 191.0 | 47.13 |
| 50K | 16,571 ± 791 | 159.3 ± 6.0 | 479.0 | 119.31 |
- Complexity: O(N) — linear scaling across 100 → 50K range
- Platform: Win32 optimization reduces syscall overhead vs cross-platform
pathlib - Parallelism: Master-worker queue distributes work evenly; 4 threads near-optimal for this I/O pattern
- Baseline: ~3.3ms per item for cold cache, improves slightly on warm cache
- Complexity: O(N*K) where K = avg keywords per item (~3-5)
- Linear trend: ~3.2μs per item at scale
- Bottleneck: String tokenization and set updates; persisted index not used on load (future optimization)
- Complexity: O(T + M log M) = extremely fast
- Observation: Sub-millisecond latency even at 50K items (in-memory hash lookup)
- Tradeoff: Achieved by holding entire keyword index in RAM
- Comparison to Windows Explorer: 0.5ms (StudyVault) vs 100-500ms (Explorer disk-based search)
- Per-Item Footprint: ~2.3-2.4 KB (items + index + UI overhead)
- Linear scaling: 100 items → 0.42 MB, 50K items → 119 MB
- 1M items extrapolation: ~2.3-2.5 GB RAM
- Bottleneck: UI widget creation (5
QTableWidgetItemper row)- Planned fix: Virtualized
QTableView+QAbstractTableModel(Phase 1 optimization)
- Planned fix: Virtualized
- Save: Monolithic
pickle.dump()— grows with N - Load: Monolithic
pickle.load()+ controller hydration (per-item loop + index rebuild) - Future: Chunked serialization (SQLite or sharded pickle) for 100K+ scalability
All benchmark charts below are rendered on log scale for both axes (log-log), which makes linear complexity appear as an approximately straight trend line instead of a misleading curved shape.
Plot files:
Import time shows linear O(N) scaling across 100 to 50K items. The master-worker pattern with Win32 API optimization achieves ~330μs per item.
Index build exhibits O(N*K) scaling where K is keywords per item (~3-5). At 50K items, index generation takes ~159ms. Note: This rebuilds from scratch on each load; persisted index restoration (Phase 2) will reduce this to <1ms.
Search performance remains sub-millisecond across all scales (10-479μs). The in-memory inverted index provides near-instant O(1) lookup, 100-1000x faster than filesystem-based search.
Save/load times grow linearly with item count. The monolithic pickle.dump/load blocks the UI; Phase 2 (chunked serialization) will enable streaming and background loading.
Memory scales perfectly linearly at ~2.3-2.4 KB per item. At 50K items, peak is 119MB. Breakdown:
- Item objects: ~500 bytes each
- Index structures: ~800 bytes each
- UI widgets: ~200 bytes each (5 QTableWidgetItem per row)
- Optimization Path: Virtualized QTableView (Phase 1) will reduce peak memory by 90% via render-on-demand
To regenerate plots with latest benchmark data:
python scripts/plot_benchmarks.py benchmark.csvPlots are saved to benchmarks/plots with DPI=200 and grid enabled.
- Master-worker file scanning architecture
- Sentinel-based worker exit (eliminated 1s timeout overhead)
- Win32 API integration (reduced syscall overhead)
-
Virtualized UI — Replace
QTableWidgetwithQTableView+QAbstractTableModel- Expected: 90% memory reduction for UI widgets
- Impact: Peak memory at 50K: 119MB → ~25MB
-
Index Restoration — Load persisted indexes instead of rebuilding
- Expected: Index build 159ms → 1ms for 50K items
- Impact: Startup latency improvement
-
Lazy Load + Progress — Background load with UI responsiveness
- Expected: 70% perceived startup time reduction
- Impact: Window shows immediately, data loads in background
-
Bulk Hydration — Single operation instead of per-item loop
- Expected: 5-10% hydration speedup
-
Chunked Serialization — Sharded pickle or SQLite for 100K+ scales
- Expected: Scales to millions of items
- Impact: Monolithic pickle.load/dump no longer bottleneck
git clone <repo>
cd StudyVault
pip install -e .Windows-specific (recommended):
pip install pywin32
python -m pywin32_postinstall -installpython -m studyvault.main# Small test (100-10K items, 2 reps each)
python -m studyvault.benchmark --scales 100 1000 5000 10000 --profile small --reps 2 --pregenerate --parallel --mem
# Full test (100-50K items, 5 reps each)
python -m studyvault.benchmark --scales 100 1000 5000 10000 20000 50000 --profile small --reps 5 --pregenerate --parallel --mem
# Generate plots
python scripts/plot_benchmarks.py benchmark.csvsrc/studyvault/
├─ main.py # App entry point + lifecycle
├─ benchmark.py # Benchmark harness
├─ controllers/
│ ├─ main_controller.py # MVC orchestration
│ └─ detail_controller.py
├─ services/
│ ├─ import_service.py # Directory scanning (uses scan_directory_async)
│ ├─ library_service.py # CRUD + undo/redo
│ └─ search_service.py # Inverted index search
├─ repositories/
│ └─ library_repository.py # Pickle I/O + LibraryData
├─ models/
│ ├─ item.py # File metadata
│ ├─ task.py # Priority queue
│ └─ memento.py # Undo/redo
├─ utils/
│ ├─ file_util.py # Directory scanning (scan_directory_async with Win32 + pathlib)
│ └─ logger.py
├─ views/
│ └─ main_window.py # PyQt6 UI
└─ resources/
└─ css/styles.qss
tests/
├─ test_services.py
├─ test_models.py
├─ test_repository.py
└─ test_utils.py
scripts/
└─ plot_benchmarks.py # Benchmark visualization
- N directories to scan = Ω(N) time inherent in the problem
- Master-worker + Win32: Minimizes constants and eliminates overhead
- Achieved: ~51μs per item at 100, ~330μs per item at 50K (slight increase due to queue overhead, acceptable)
- Decision: Hold entire inverted index in RAM for O(1) lookups
- Result: 0.5ms queries vs Explorer's 100-500ms
- Cost: ~1 MB per 420 items for index structure
- Justification: For a desktop app serving 1-10K items, memory cost is acceptable; latency improvement is dramatic
- Per-item footprint stays constant at 2.3-2.4 KB across scales
- Not O(N log N) or worse — truly linear
- Main component breakdown:
- Item object: ~500 bytes
- Index structures: ~800 bytes
- UI widgets (5 QTableWidgetItem per row): ~200 bytes per item (planned: remove via virtualization)
| Issue | Current Workaround | Future Solution |
|---|---|---|
| Monolithic pickle load | Show loading message | Stream deserialization + lazy loading |
| 5N widget objects on screen | Memory limits < 1M items | Virtualized QTableView (Phase 1) |
| Index always rebuilt on load | Discard persisted index | Index restoration with integrity checks (Phase 2) |
| No true 100K+ scalability | Use smaller datasets | Chunked serialization (Phase 5) |
pytest tests/Unit tests cover:
- Import service (directory scanning, deduplication)
- Library service (CRUD, undo/redo, task ordering)
- Search service (indexing, queries)
- Repository (save/load, corruption handling)
- Models (Item, Task validation)
Educational/Reference Implementation
All results generated on:
- OS: Windows 11
- Python: 3.10+
- pywin32: ✓ Installed
- Methodology:
--pregenerate --parallel --memflags- Files pre-generated before timer starts (eliminates generation variance)
- Parallel import using master-worker + Win32 API
- Memory tracked via
tracemalloc(peak resident set)
For reproducible results, clear OS disk cache and close background apps before running full benchmarks.





