Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Asynchronous Build System

techy4shri edited this page Nov 29, 2025 · 2 revisions

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:

# ❌ 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

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

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

@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

@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

@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

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

# 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

# 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

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

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:

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

# 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

# Old PyQt style - NOT RECOMMENDED
class BuildThread(QThread):
 def run(self):
 result = build_project(...)

Problem: Tight coupling, harder to test

[x] QObject + moveToThread

# 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

# WRONG - crashes or undefined behavior
def run(self):
 self.main_window.statusLabel.setText("Building...") # ❌

[x] Signal-Based Updates

# 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

# 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

# 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
Previous: Architecture Overview

Clone this wiki locally

AltStyle によって変換されたページ (->オリジナル) /