Skip to content

Commit ed3800e

Browse files
committed
feat: Implement lazy loading for core modules and enhance process handling in Python API
These changes from this commit and the last implement huge performance improvements. Will allow for multiple processes to happen at once as well.
1 parent eed524c commit ed3800e

4 files changed

Lines changed: 386 additions & 79 deletions

File tree

EncodeForge/src/main/java/com/encodeforge/MainApp.java

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.encodeforge.controller.MainController;
55
import com.encodeforge.service.DependencyManager;
66
import com.encodeforge.service.PythonBridge;
7+
import com.encodeforge.service.PythonProcessPool;
78
import com.encodeforge.util.PathManager;
89
import com.encodeforge.util.ResourceExtractor;
910
import javafx.application.Application;
@@ -31,9 +32,11 @@ public class MainApp extends Application {
3132
private static final String VERSION = "0.3.1";
3233

3334
private DependencyManager dependencyManager;
34-
private PythonBridge pythonBridge;
35+
private PythonBridge pythonBridge; // Legacy - will be replaced by processPool
36+
private PythonProcessPool processPool;
3537
private MainController controller;
3638
private boolean pendingDependencyInstall = false;
39+
private boolean useProcessPool = true; // Feature flag for gradual rollout
3740

3841
@Override
3942
public void init() throws Exception {
@@ -103,14 +106,31 @@ public void init() throws Exception {
103106
}
104107

105108
/**
106-
* Initialize runtime components (Python bridge, etc.)
109+
* Initialize runtime components (Python bridge or process pool)
107110
*/
108111
private void initializeRuntime() throws IOException {
109112
try {
110-
// Initialize Python bridge (will use bundled or system Python)
111-
pythonBridge = new PythonBridge(dependencyManager);
112-
pythonBridge.start();
113-
logger.info("Python bridge initialized");
113+
if (useProcessPool) {
114+
// NEW: Multi-process pool for concurrent operations
115+
logger.info("Initializing Python process pool (multi-worker mode)");
116+
processPool = new PythonProcessPool(dependencyManager, 4); // 4 workers for good balance
117+
118+
// Start asynchronously - UI won't be blocked
119+
processPool.startAsync()
120+
.thenRun(() -> logger.info("Python process pool started successfully"))
121+
.exceptionally(error -> {
122+
logger.error("Failed to start process pool", error);
123+
return null;
124+
});
125+
126+
logger.info("Python process pool initialization started (async)");
127+
} else {
128+
// LEGACY: Single Python bridge (backwards compatibility)
129+
logger.info("Initializing Python bridge (legacy single-worker mode)");
130+
pythonBridge = new PythonBridge(dependencyManager);
131+
pythonBridge.start();
132+
logger.info("Python bridge initialized");
133+
}
114134
} catch (IOException e) {
115135
// Check if this is a Python not found error
116136
if (e.getMessage() != null && e.getMessage().contains("Python not found")) {
@@ -178,7 +198,13 @@ public void start(Stage primaryStage) {
178198
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/MainView.fxml"));
179199

180200
// Create controller and inject dependencies
181-
controller = new MainController(pythonBridge);
201+
if (useProcessPool) {
202+
// NEW: Pass process pool to controller
203+
controller = new MainController(processPool);
204+
} else {
205+
// LEGACY: Pass Python bridge to controller
206+
controller = new MainController(pythonBridge);
207+
}
182208
loader.setController(controller);
183209

184210
// Load scene
@@ -267,7 +293,11 @@ public void stop() throws Exception {
267293
controller.shutdown();
268294
}
269295

270-
if (pythonBridge != null) {
296+
if (useProcessPool && processPool != null) {
297+
logger.info("Shutting down Python process pool");
298+
processPool.shutdown();
299+
} else if (pythonBridge != null) {
300+
logger.info("Shutting down Python bridge");
271301
pythonBridge.shutdown();
272302
}
273303

EncodeForge/src/main/java/com/encodeforge/controller/MainController.java

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.encodeforge.model.ConversionSettings;
55
import com.encodeforge.service.DependencyManager;
66
import com.encodeforge.service.PythonBridge;
7+
import com.encodeforge.service.PythonProcessPool;
78
import com.encodeforge.util.HardwareDetector;
89
import com.encodeforge.util.PathManager;
910
import com.encodeforge.util.StatusManager;
@@ -49,14 +50,16 @@
4950
import java.util.Set;
5051
import java.util.concurrent.CompletableFuture;
5152
import java.util.concurrent.TimeoutException;
53+
import java.util.concurrent.TimeUnit;
5254

5355
/**
5456
* Main window controller for the modernized UI
5557
*/
5658
public class MainController {
5759
private static final Logger logger = LoggerFactory.getLogger(MainController.class);
5860

59-
private final PythonBridge pythonBridge;
61+
private final PythonBridge pythonBridge; // Legacy mode
62+
private final PythonProcessPool processPool; // Multi-worker mode
6063
private final DependencyManager dependencyManager;
6164
private final StatusManager statusManager;
6265
// Separate queues for different states
@@ -299,8 +302,37 @@ private enum ResizeDirection {
299302
@FXML private Tab fileInfoTab;
300303
@FXML private Tab logsTab;
301304

305+
/**
306+
* Constructor for legacy single-worker mode
307+
*/
302308
public MainController(PythonBridge pythonBridge) {
303309
this.pythonBridge = pythonBridge;
310+
this.processPool = null; // Legacy mode
311+
312+
// Get dependency manager from MainApp (will be passed in future refactor)
313+
// For now, create a new one
314+
try {
315+
this.dependencyManager = new DependencyManager();
316+
} catch (IOException e) {
317+
logger.error("Failed to initialize DependencyManager", e);
318+
throw new RuntimeException("Could not initialize dependency manager", e);
319+
}
320+
321+
this.statusManager = StatusManager.getInstance();
322+
323+
// Load settings from disk
324+
this.settings = ConversionSettings.load();
325+
logger.info("Settings loaded from: {}", ConversionSettings.getSettingsFilePath());
326+
327+
logger.info("MainController initialized with PythonBridge (legacy mode)");
328+
}
329+
330+
/**
331+
* Constructor for new multi-worker mode
332+
*/
333+
public MainController(PythonProcessPool processPool) {
334+
this.pythonBridge = null; // Multi-worker mode
335+
this.processPool = processPool;
304336

305337
// Get dependency manager from MainApp (will be passed in future refactor)
306338
// For now, create a new one
@@ -317,7 +349,7 @@ public MainController(PythonBridge pythonBridge) {
317349
this.settings = ConversionSettings.load();
318350
logger.info("Settings loaded from: {}", ConversionSettings.getSettingsFilePath());
319351

320-
// FFmpeg is now managed by DependencyManager, no need for embedded setup here
352+
logger.info("MainController initialized with PythonProcessPool (multi-worker mode)");
321353
}
322354

323355
@FXML
@@ -2532,8 +2564,137 @@ private void resetProcessingState() {
25322564
* FFmpeg detection now uses Java-side DependencyManager instead of Python.
25332565
*/
25342566
private void updateAllStatus() {
2567+
// Use parallel execution if process pool is available
2568+
if (processPool != null) {
2569+
updateAllStatusParallel();
2570+
} else {
2571+
updateAllStatusSequential();
2572+
}
2573+
}
2574+
2575+
/**
2576+
* Parallel version using PythonProcessPool for fast concurrent checks
2577+
*/
2578+
private void updateAllStatusParallel() {
2579+
try {
2580+
logger.info("Starting parallel status check (multi-process mode)");
2581+
long startTime = System.currentTimeMillis();
2582+
2583+
// Sync settings with Python backend (quick operation)
2584+
try {
2585+
JsonObject settingsParams = new JsonObject();
2586+
settingsParams.add("settings", settings.toJson());
2587+
processPool.submitQuickTask("update_settings", settingsParams).get(2, TimeUnit.SECONDS);
2588+
logger.info("Settings synchronized with Python backend");
2589+
} catch (Exception e) {
2590+
logger.error("Failed to sync settings with Python backend", e);
2591+
}
2592+
2593+
// Check FFmpeg using Java DependencyManager (parallel with Python checks)
2594+
CompletableFuture<Boolean> ffmpegCheckFuture = CompletableFuture.supplyAsync(() -> {
2595+
try {
2596+
boolean available = dependencyManager.checkFFmpeg().get();
2597+
logger.info("FFmpeg check completed: {}", available);
2598+
return available;
2599+
} catch (Exception e) {
2600+
logger.error("Error checking FFmpeg via DependencyManager", e);
2601+
return false;
2602+
}
2603+
});
2604+
2605+
// Parallel Python backend checks
2606+
CompletableFuture<JsonObject> getAllStatusFuture = CompletableFuture.supplyAsync(() -> {
2607+
try {
2608+
JsonObject result = processPool.submitQuickTask("get_all_status").get(5, TimeUnit.SECONDS);
2609+
logger.info("get_all_status completed");
2610+
return result;
2611+
} catch (Exception e) {
2612+
logger.error("Error getting all status", e);
2613+
JsonObject error = new JsonObject();
2614+
error.addProperty("status", "error");
2615+
error.addProperty("message", e.getMessage());
2616+
return error;
2617+
}
2618+
});
2619+
2620+
// Wait for all checks to complete
2621+
CompletableFuture<Void> allChecks = CompletableFuture.allOf(
2622+
ffmpegCheckFuture,
2623+
getAllStatusFuture
2624+
);
2625+
2626+
allChecks.get(10, TimeUnit.SECONDS); // Wait for all checks
2627+
2628+
// Get results
2629+
boolean ffmpegAvailable = ffmpegCheckFuture.get();
2630+
String ffmpegVersion = "Not Found";
2631+
2632+
if (ffmpegAvailable) {
2633+
java.nio.file.Path ffmpegPath = dependencyManager.getInstalledFFmpegPath();
2634+
if (ffmpegPath != null) {
2635+
ffmpegVersion = "Found";
2636+
logger.info("FFmpeg detected via DependencyManager at: {}", ffmpegPath);
2637+
} else {
2638+
ffmpegVersion = "System PATH";
2639+
logger.info("FFmpeg detected in system PATH");
2640+
}
2641+
}
2642+
2643+
JsonObject response = getAllStatusFuture.get();
2644+
2645+
if (response.has("status") && !response.get("status").isJsonNull()
2646+
&& response.get("status").getAsString().equals("error")) {
2647+
String errorMsg = response.has("message") && !response.get("message").isJsonNull()
2648+
? response.get("message").getAsString()
2649+
: "Unknown error";
2650+
logger.error("Error getting Python status: " + errorMsg);
2651+
}
2652+
2653+
// Update FFmpeg info in response (override Python's check with Java's)
2654+
if (response.has("ffmpeg")) {
2655+
JsonObject ffmpegInfo = response.getAsJsonObject("ffmpeg");
2656+
ffmpegInfo.addProperty("available", ffmpegAvailable);
2657+
if (ffmpegAvailable && ffmpegInfo.has("version")) {
2658+
ffmpegVersion = ffmpegInfo.get("version").getAsString();
2659+
}
2660+
}
2661+
2662+
// Update StatusManager with the consolidated response
2663+
com.encodeforge.util.StatusManager.getInstance().updateFromResponse(response);
2664+
2665+
// Update UI on JavaFX thread
2666+
final boolean finalFfmpegAvailable = ffmpegAvailable;
2667+
final String finalFfmpegVersion = ffmpegVersion;
2668+
2669+
Platform.runLater(() -> {
2670+
updateFFmpegStatus(finalFfmpegAvailable, finalFfmpegVersion);
2671+
updateUIFromStatus();
2672+
});
2673+
2674+
long elapsedMs = System.currentTimeMillis() - startTime;
2675+
logger.info("Parallel status check completed in {}ms", elapsedMs);
2676+
2677+
} catch (TimeoutException e) {
2678+
logger.error("Status check timed out", e);
2679+
Platform.runLater(() -> {
2680+
log("ERROR: Status check timed out - " + e.getMessage());
2681+
updateFFmpegStatus(false, "Timeout");
2682+
});
2683+
} catch (Exception e) {
2684+
logger.error("Unexpected error in parallel status check", e);
2685+
Platform.runLater(() -> {
2686+
log("ERROR: " + e.getMessage());
2687+
updateFFmpegStatus(false, "Error");
2688+
});
2689+
}
2690+
}
2691+
2692+
/**
2693+
* Sequential version using legacy PythonBridge (backward compatibility)
2694+
*/
2695+
private void updateAllStatusSequential() {
25352696
try {
2536-
logger.info("Starting consolidated status check");
2697+
logger.info("Starting consolidated status check (legacy mode)");
25372698

25382699
// First, sync settings with Python backend
25392700
try {
@@ -6394,7 +6555,13 @@ private void checkForOngoingConversions() {
63946555
try {
63956556
logger.info("Checking for ongoing conversions from previous session...");
63966557

6397-
JsonObject response = pythonBridge.checkOngoingConversion();
6558+
// Use appropriate bridge based on mode
6559+
JsonObject response;
6560+
if (processPool != null) {
6561+
response = processPool.submitQuickTask("check_ongoing_conversion").get(3, TimeUnit.SECONDS);
6562+
} else {
6563+
response = pythonBridge.checkOngoingConversion();
6564+
}
63986565

63996566
if (response == null || !response.has("status")) {
64006567
logger.warn("Invalid response from check_ongoing_conversion");

0 commit comments

Comments
 (0)