# Asynchronous Build System CppLab IDE uses a fully asynchronous build system to ensure the UI remains responsive during compilation. This document explains the architecture and implementation details. ## Problem Statement ### The Freezing Problem Traditional IDEs often execute builds synchronously: ```python # ❌ Blocking approach (old method) def on_build_project(self): result = build_project(config, toolchains) # BLOCKS for 1-3 seconds update_ui(result) ``` **Issues:** - UI freezes during compilation (1-3 seconds) - User cannot interact with menus/windows - Feels unresponsive and unprofessional - Cannot cancel or monitor progress ## Solution: Threading with Qt ### Architecture ``` ┌─────────────────┐ │ MainWindow │ ← Main Thread (Qt Event Loop) │ │ │ build_current()│ └────────┬────────┘ │ creates ↓ ┌─────────────────┐ │ QThread │ ← Background Thread │ │ │ BuildWorker │ │ .run() │ └────────┬────────┘ │ calls ↓ ┌─────────────────┐ │ builder.py │ ← Core Build Logic │ │ │ build_project() │ │ build_single_ │ │ _file() │ └─────────────────┘ ``` ## Implementation ### 1. BuildWorker Class **Location**: `src/cpplab/app.py` ```python class BuildWorker(QObject): """Worker that runs build/check operations in a background thread.""" # Signals (thread-safe communication) started = pyqtSignal() finished = pyqtSignal(object) # BuildResult error = pyqtSignal(str) def __init__(self, toolchains, project_config=None, source_path=None, force_rebuild=False, check_only=False): super().__init__() self.toolchains = toolchains self.project_config = project_config self.source_path = source_path self.force_rebuild = force_rebuild self.check_only = check_only @pyqtSlot() def run(self): """Execute the build/check operation.""" try: self.started.emit() # Determine operation if self.check_only: if self.project_config: result = check_project(self.project_config, self.toolchains) else: result = check_single_file(self.source_path, self.toolchains) else: if self.project_config: result = build_project(self.project_config, self.toolchains, self.force_rebuild) else: result = build_single_file(self.source_path, self.toolchains) self.finished.emit(result) except Exception as e: self.error.emit(str(e)) ``` **Key Points:** - Inherits `QObject` (not QThread) - modern PyQt6 pattern - Uses Qt signals for thread-safe communication - `@pyqtSlot()` decorator for proper slot connection - Handles both project and standalone builds - Supports syntax-only checks (`check_only=True`) ### 2. Thread Management in MainWindow **Location**: `src/cpplab/app.py` ```python def start_build_task(self, *, project_config=None, source_path=None, force_rebuild=False, check_only=False): """Start a background build/check task if none is running.""" # Prevent concurrent builds if self.build_in_progress: QMessageBox.information(self, "Build In Progress", "A build is already running. Please wait for it to complete.") return # Save files before building if not check_only: self.on_save_all() # Create thread and worker thread = QThread(self) worker = BuildWorker( toolchains=self.toolchains, project_config=project_config, source_path=source_path, force_rebuild=force_rebuild, check_only=check_only ) # Move worker to thread (critical!) worker.moveToThread(thread) # Connect signals thread.started.connect(worker.run) # Start work when thread starts worker.started.connect(self.on_build_started) # Update UI worker.finished.connect(self.on_build_finished) # Handle result worker.error.connect(self.on_build_error) # Handle errors worker.finished.connect(thread.quit) # Stop thread worker.finished.connect(worker.deleteLater) # Clean up worker thread.finished.connect(thread.deleteLater) # Clean up thread # Store reference and start self.current_build_thread = thread self.build_in_progress = True thread.start() ``` **Thread Safety Pattern:** 1. Create `QThread` object 2. Create `BuildWorker` (QObject) 3. **Move worker to thread** with `moveToThread()` 4. Connect signals/slots 5. Start thread with `thread.start()` ### 3. Signal Handlers #### Build Started ```python @pyqtSlot() def on_build_started(self): """Handle build start - update UI to show build in progress.""" self.statusBuildLabel.setText("Building...") # Disable actions to prevent spam self.buildProjectAction.setEnabled(False) self.buildAndRunAction.setEnabled(False) self.runProjectAction.setEnabled(False) # Clear output and switch to Build tab self.output_panel.clear_output() self.output_panel.append_output("=== Build Started ===\n") self.outputDockWidget.setVisible(True) self.outputTabWidget.setCurrentIndex(0) ``` #### Build Finished ```python @pyqtSlot(object) def on_build_finished(self, result: BuildResult): """Handle build completion - update UI with results.""" # Re-enable actions self.buildProjectAction.setEnabled(True) self.buildAndRunAction.setEnabled(True) self.runProjectAction.setEnabled(True) self.build_in_progress = False self.current_build_thread = None # Display output if result.command: self.output_panel.append_output(f"\nCommand: {' '.join(result.command)}\n") if result.stdout: self.output_panel.append_output("\n--- Standard Output ---\n") self.output_panel.append_output(result.stdout) if result.stderr: self.output_panel.append_output("\n--- Standard Error ---\n") self.output_panel.append_output(result.stderr) # Update status bar with timing if result.success: msg = "Build succeeded" else: msg = "Build failed" if hasattr(result, "elapsed_ms") and self.settings.show_build_elapsed: msg += f" in {result.elapsed_ms:.0f} ms" if result.skipped: msg = "Build skipped (up to date)" self.statusBuildLabel.setText(msg) # Handle Build & Run workflow if result.success and self._pending_run_after_build: self._pending_run_after_build = False self.run_current() ``` #### Build Error ```python @pyqtSlot(str) def on_build_error(self, message: str): """Handle build error.""" self.build_in_progress = False self.current_build_thread = None # Re-enable actions self.buildProjectAction.setEnabled(True) self.buildAndRunAction.setEnabled(True) self.runProjectAction.setEnabled(True) self.statusBuildLabel.setText("Build error") QMessageBox.critical(self, "Build Error", message) ``` ## Build & Run Workflow ### Challenge How to build first, then run if successful? ### Solution: Pending Flag ```python def on_build_and_run(self): """Build and run current project or standalone file.""" self._pending_run_after_build = True self.build_current() # Later in on_build_finished: if result.success and self._pending_run_after_build: self._pending_run_after_build = False self.run_current() ``` **Flow:** 1. User presses F5 (Build & Run) 2. Set `_pending_run_after_build = True` 3. Start async build 4. Build completes → `on_build_finished()` 5. Check flag, if true → call `run_current()` 6. `run_current()` uses non-blocking `Popen` (already async) ## Concurrency Control ### Preventing Parallel Builds ```python # State flag self.build_in_progress: bool = False # Check before starting if self.build_in_progress: QMessageBox.information(self, "Build In Progress", "A build is already running. Please wait for it to complete.") return # Set flag self.build_in_progress = True thread.start() # Clear flag when done def on_build_finished(self, result): self.build_in_progress = False ``` ### Thread Cleanup ```python # Worker auto-deletes worker.finished.connect(worker.deleteLater) # Thread auto-deletes thread.finished.connect(thread.deleteLater) # Reference cleared self.current_build_thread = None ``` ### Application Exit ```python def closeEvent(self, event): """Handle application close event.""" if self.build_in_progress: reply = QMessageBox.question( self, "Build in progress", "A build is currently running. Do you really want to exit?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.No: event.ignore() return super().closeEvent(event) ``` ## Status Bar Integration ### Setup ```python def _setup_widgets(self): # ... # Add build status label to status bar self.statusBuildLabel = QLabel("Ready") self.statusbar.addPermanentWidget(self.statusBuildLabel) ``` ### Status Updates ``` Ready ↓ (build starts) Building... ↓ (build completes) Build succeeded in 1234 ms ↓ (or if failed) Build failed in 567 ms ↓ (or if skipped) Build skipped (up to date) ``` ### Timing Display Controlled by user settings: ```python if self.settings.show_build_elapsed: msg += f" in {result.elapsed_ms:.0f} ms" ``` ## Benefits ### 1. Responsive UI [x] No freezing during compilation [x] User can resize/move windows [x] Menus remain accessible [x] Professional user experience ### 2. Build Feedback [x] Real-time status updates [x] Build timing visible [x] Clear success/failure indication [x] Elapsed time transparency ### 3. Safety [x] Cannot start multiple builds [x] Warns before closing during build [x] Threads properly cleaned up [x] Exception handling in worker ### 4. Workflow [x] Build & Run works seamlessly [x] Can queue Run after Build [x] Non-blocking Run (Popen) [x] Build/Run buttons disabled during build ## Performance Impact ### Thread Overhead - **Thread creation**: ~10ms (one-time) - **Signal emission**: <1ms (negligible) - **UI updates**: ~5ms (batched by Qt) - **Total overhead**: ~15ms (vs 1000-3000ms build time = <2%) ### Memory - `QThread`: ~50KB per thread - `BuildWorker`: ~10KB - Only 1 thread active at a time - Auto-cleanup prevents leaks ### Comparison | Aspect | Synchronous | Asynchronous | |--------|-------------|--------------| | UI Responsiveness | ❌ Freezes 1-3s | [x] Always responsive | | User Experience | Poor | Professional | | Build Timing | Hidden | Visible in status | | Concurrent Builds | Possible (bad) | Prevented (good) | | Code Complexity | Simple | Moderate | | Thread Overhead | None | ~15ms (<2%) | ## Testing ### Manual Testing 1. Open large project 2. Press F7 (Build) 3. During build: - Try resizing window [x] - Try opening menus [x] - Try starting another build (should be blocked) [x] 4. Check status bar shows "Building..." [x] 5. After build, check timing appears [x] ### Unit Testing ```python # Future: Test signal emissions def test_build_worker_signals(): worker = BuildWorker(toolchains, project_config=config) started_emitted = False finished_emitted = False def on_started(): nonlocal started_emitted started_emitted = True def on_finished(result): nonlocal finished_emitted finished_emitted = True assert result.success worker.started.connect(on_started) worker.finished.connect(on_finished) worker.run() assert started_emitted assert finished_emitted ``` ## Common Pitfalls (Avoided) ### ❌ Subclassing QThread ```python # Old PyQt style - NOT RECOMMENDED class BuildThread(QThread): def run(self): result = build_project(...) ``` **Problem**: Tight coupling, harder to test ### [x] QObject + moveToThread ```python # Modern PyQt6 style - RECOMMENDED class BuildWorker(QObject): def run(self): result = build_project(...) worker.moveToThread(thread) ``` **Benefit**: Loose coupling, easier to test ### ❌ Direct UI Updates from Thread ```python # WRONG - crashes or undefined behavior def run(self): self.main_window.statusLabel.setText("Building...") # ❌ ``` ### [x] Signal-Based Updates ```python # CORRECT - thread-safe def run(self): self.started.emit() # [x] Signal triggers UI update in main thread ``` ## Future Enhancements ### Streaming Output Currently: Output displayed after build completes Future: Stream stdout/stderr line-by-line during build ```python # Future: Real-time output class BuildWorker(QObject): output_line = pyqtSignal(str) # Emit each line def run(self): process = subprocess.Popen(cmd, stdout=PIPE, ...) for line in process.stdout: self.output_line.emit(line.decode()) ``` ### Build Cancellation Currently: Build runs to completion Future: Cancel button to terminate build ```python # Future: Cancellable builds class BuildWorker(QObject): def __init__(self): self._cancelled = False def cancel(self): self._cancelled = True if self._process: self._process.terminate() ``` ### Progress Bar Currently: Indeterminate "Building..." message Future: Progress percentage for multi-file projects ### Build Queue Builds now run in parallel, subject to a configurable global concurrency limit (Settings → Build → Max concurrent builds). Independent projects/files are built concurrently while builds targeting the same project are serialized to avoid race conditions and artifact conflicts. Each concurrent build gets its own output entry and can be cancelled individually; the queue enforces per-project ordering and global throttling. --- **Next**: [Build System Details](Build-System-Details.md) **Previous**: [Architecture Overview](Architecture-Overview.md)