Run a simulation in its own thread#532
Conversation
There was a problem hiding this comment.
Pull request overview
Enables running libOpenCOR simulations asynchronously (in a background thread) so the UI can remain responsive and support progressive/interactive simulation updates, aligning with Issue #63’s goal of “gradual” simulation execution similar to legacy OpenCOR.
Changes:
- Replaced the synchronous
sedInstanceRunAPI with a threaded run lifecycle: status/progress/start/wait/pause/resume/stop (C++ N-API + TS wrapper + preload bridge). - Updated the Simulation Experiment view to support run/pause/resume/stop controls and a progress bar, including polling to keep UI responsive.
- Adjusted web initialization and dev-server configuration to support Emscripten pthread requirements (same-origin loading + COOP/COEP in dev), plus minor plotting improvements (optional axis ranges).
Reviewed changes
Copilot reviewed 17 out of 20 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/renderer/vite.config.ts | Adds COOP/COEP headers for dev server and proxies /libopencor-wasm to serve WASM assets same-origin. |
| src/renderer/src/libopencor/src/sed.h | Updates N-API surface to threaded run lifecycle functions. |
| src/renderer/src/libopencor/src/sed.cpp | Implements new N-API exports for status/progress/start/wait/pause/resume/stop. |
| src/renderer/src/libopencor/src/main.cpp | Exposes the new SedInstance run lifecycle functions to JS. |
| src/renderer/src/libopencor/locSedApi.ts | Adds ESedInstanceStatus and SedInstance lifecycle methods for both C++ and WASM backends. |
| src/renderer/src/libopencor/locApi.ts | Extends the C++ bridge interface with the new SedInstance lifecycle functions. |
| src/renderer/src/components/widgets/GraphPanelWidget.vue | Adds optional axis range constraints into Plotly layout. |
| src/renderer/src/components/views/SimulationExperimentView.vue | Implements run/pause/resume/stop UI, progress polling, and interactive cancellation/restart logic. |
| src/renderer/src/components/propertyEditors/SimulationPropertyEditor.vue | Adds a disabled prop passthrough to prevent edits while running. |
| src/renderer/src/components/propertyEditors/PropertyEditor.vue | Disables DataTable cell editing when disabled is true. |
| src/renderer/src/common/initialisation.ts | Switches WASM loading to a same-origin proxied path to support pthread workers. |
| src/renderer/src/common/constants.ts | Adds VERY_SHORT_DELAY for tight polling intervals. |
| src/renderer/package.json | Bumps app + libOpenCOR types versions and dev deps. |
| src/renderer/index.html | Updates CSP to allow same-origin workers (worker-src 'self'). |
| src/renderer/bun.lock | Lockfile updates for renderer package. |
| src/preload/index.ts | Bridges new SedInstance lifecycle APIs into window.locApi. |
| src/extern/corsProxy.js | Refactors worker CORS proxy with helpers; adjusts error handling behavior. |
| package.json | Bumps root app version and dev deps. |
| bun.lock | Lockfile updates for root package. |
Comments suppressed due to low confidence (2)
src/renderer/src/components/views/SimulationExperimentView.vue:1602
- The interactive run path starts a background run and waits for the status to return to IDLE, but never calls
waitForRun(). IfwaitForRun()is required to join/finalise the run (and surface timing/errors deterministically), this can leave the worker/thread unjoined and accumulate state across updates/cancellations.
interactiveInstance.startRun();
await waitWhileRunning(interactiveInstance);
// Check if we have been superseded by a newer call while the simulation was running.
if (currentSimulationGeneration !== interactiveSimulationGeneration) {
return;
src/renderer/src/components/views/SimulationExperimentView.vue:535
onStop()forcesstandardSimulationStatusto IDLE immediately after callingstopRun(). If stopping is asynchronous, this can temporarily re-enable settings/UI while the simulation is still winding down. Let the polling loop update the status (or read it back from the instance) instead of forcing IDLE.
const onStop = (): void => {
standardRunAborted = true;
standardInstance.stopRun();
standardSimulationStatus.value = locSedApi.ESedInstanceStatus.IDLE;
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
4a1185a to
9e0417e
Compare
qwe
We don't need to yield to the UI thread.
…n their own thread.
This model takes about half a second to run, which is good for testing UI interaction when running a simulation. The plan is indeed to cancel a simulation and starting a new one if simulation inputs change.
…uts keep on changing.
…ore the underlying C++ thread has fully stopped.
…pping them in `Float64Array`.
…ck to waitWhileRunning().
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 22 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (4)
src/renderer/src/components/views/SimulationExperimentView.vue:1630
SedInstance.startRun()returns a boolean, but it’s ignored here. If a background run fails to start, continuing to awaitwaitWhileRunning()can mask the failure and make debugging harder. Consider handling thefalsecase and surfacing issues early.
interactiveInstance.startRun();
await waitWhileRunning(interactiveInstance).promise;
src/renderer/src/components/views/SimulationExperimentView.vue:377
- After introducing
cancelled,onStatusChangeshould not be invoked once cancellation has happened (e.g., on component unmount), otherwise it can still write to stale refs.
if (status !== lastStatus) {
lastStatus = status;
onStatusChange?.(status);
}
src/renderer/src/components/views/SimulationExperimentView.vue:388
- After introducing
cancelled, suppress progress updates when cancelled sowaitWhileRunning()doesn’t keep mutating state after unmount.
switch (status) {
case locSedApi.ESedInstanceStatus.RUNNING:
onProgress?.(100 * instance.progress());
setTimeout(poll, VERY_SHORT_DELAY);
break;
src/renderer/src/components/views/SimulationExperimentView.vue:402
- The IDLE branch still schedules a progress reset timer and calls
onProgresseven after cancellation. Gate this block on!cancelledso unmount cancellation actually prevents late progress writes.
default: // locSedApi.ESedInstanceStatus.IDLE:
if (onProgress && !standardRunAborted) {
onProgress(100);
// Reset the progress bar after a short delay.
progressResetTimer = setTimeout(() => {
onProgress?.(0);
}, MEDIUM_DELAY);
}
Indeed, this keeps existing axis config intact while making the update safer for cases where axis objects may be undefined and more reliable for reactive layout updates.
…to start. Indeed, if a simulation fails to start, the standard path now refreshes the displayed status and exits early, while the interactive path returns immediately instead of waiting on a run that never started.
Replace the hardcoded libOpenCOR version define with a base-URL define and use it when loading the WASM bundle. This keeps the same-origin dev proxy behavior while allowing production web builds to point libOpenCOR assets at a custom host or path, and drops the unused define from the library Vite config.
This allows to exist the poll loop early and to resolve immediately when cancelled. Also guard delayed progress reset callbacks so they no longer emit `onProgress(0)` after cancellation.
Indeed, this avoids clearing `standardData` (axis titles and traces) at run start so existing plotting state is preserved.
Fixes #63.