Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow scripts to be cancelable #278

Merged
merged 4 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/stan-wasm-server/src/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def setup_logger() -> None:
allow_origins=[
"https://stan-playground.flatironinstitute.org",
"https://stan-playground.vercel.app",
"https://stan-playground-staging.vercel.app",
"http://127.0.0.1:3000", # yarn dev
"http://127.0.0.1:4173", # yarn preview
],
Expand Down Expand Up @@ -106,7 +107,13 @@ async def download_file(
file_path = model_dir / filename
if not file_path.is_file():
raise FileNotFoundError(f"File not found: {file_path}")
return FileResponse(file_path)
return FileResponse(
file_path,
headers={
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Opener-Policy": "same-origin",
},
)


@app.post("/compile")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const DataPyPanel: FunctionComponent = () => {
[consoleRef, onData, onStatus],
);

const { run } = usePyodideWorker(callbacks);
const { run, cancel } = usePyodideWorker(callbacks);

const handleRun = useCallback(
(code: string) => {
Expand Down Expand Up @@ -62,6 +62,7 @@ const DataPyPanel: FunctionComponent = () => {
language="python"
status={status}
onRun={handleRun}
onCancel={cancel}
runnable={true}
notRunnableReason=""
onHelp={handleHelp}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const handleHelp = () =>
const DataRPanel: FunctionComponent = () => {
const { consoleRef, status, onStatus, onData } = useDataGenState("r");

const { run } = useWebR({ consoleRef, onStatus, onData });
const { run, cancel } = useWebR({ consoleRef, onStatus, onData });

const handleRun = useCallback(
async (code: string) => {
clearOutputDivs(consoleRef);
Expand All @@ -40,6 +41,7 @@ const DataRPanel: FunctionComponent = () => {
language="r"
status={status}
onRun={handleRun}
onCancel={cancel}
runnable={true}
notRunnableReason=""
onHelp={handleHelp}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const AnalysisPyPanel: FunctionComponent<NeedsLatestRun> = ({ latestRun }) => {
[consoleRef, imagesRef, onStatus],
);

const { run } = usePyodideWorker(callbacks);
const { run, cancel } = usePyodideWorker(callbacks);

const handleRun = useCallback(
(code: string) => {
Expand Down Expand Up @@ -69,6 +69,7 @@ const AnalysisPyPanel: FunctionComponent<NeedsLatestRun> = ({ latestRun }) => {
language="python"
status={status}
onRun={handleRun}
onCancel={cancel}
runnable={runnable}
notRunnableReason={notRunnableReason}
imagesRef={imagesRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AnalysisRPanel: FunctionComponent<NeedsLatestRun> = ({ latestRun }) => {
files,
} = useAnalysisState(latestRun);

const { run } = useWebR({ consoleRef, imagesRef, onStatus });
const { run, cancel } = useWebR({ consoleRef, imagesRef, onStatus });

const handleRun = useCallback(
async (userCode: string) => {
Expand All @@ -48,6 +48,7 @@ const AnalysisRPanel: FunctionComponent<NeedsLatestRun> = ({ latestRun }) => {
language="r"
status={status}
onRun={handleRun}
onCancel={cancel}
runnable={runnable}
notRunnableReason={notRunnableReason}
imagesRef={imagesRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ const DrawsTablePanel: FunctionComponent<DrawsTableProps> = ({
<TableCell key="chain">Chain</TableCell>
<TableCell key="draw">Draw</TableCell>
{paramNames.map((name, i) => (
<TableCell key={i}>{name}</TableCell>
<TableCell padding="checkbox" key={i}>
{name}
</TableCell>
))}
</TableRow>
</SuccessColoredTableHead>
Expand All @@ -105,7 +107,9 @@ const DrawsTablePanel: FunctionComponent<DrawsTableProps> = ({
<TableCell>{drawChainIds[i]}</TableCell>
<TableCell>{drawNumbers[i]}</TableCell>
{formattedDraws.map((draw, j) => (
<TableCell key={j}>{draw[i]}</TableCell>
<TableCell padding="checkbox" key={j}>
{draw[i]}
</TableCell>
))}
</SuccessBorderedTableRow>
))}
Expand Down
41 changes: 35 additions & 6 deletions gui/src/app/components/FileEditor/ScriptEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FunctionComponent, RefObject, useCallback, use, useMemo } from "react";

import { Help, PlayArrow } from "@mui/icons-material";
import { Close, Help, PlayArrow } from "@mui/icons-material";
import Box from "@mui/material/Box";
import { Split } from "@geoffcox/react-splitter";
import { useMonaco } from "@monaco-editor/react";
Expand All @@ -22,6 +22,7 @@ export type ScriptEditorProps = {
filename: FileNames;
dataKey: ProjectKnownFiles;
onRun: (code: string) => void;
onCancel?: () => void;
runnable: boolean;
notRunnableReason?: string;
onHelp?: () => void;
Expand All @@ -35,6 +36,7 @@ const ScriptEditor: FunctionComponent<ScriptEditorProps> = ({
filename,
dataKey,
onRun,
onCancel,
runnable,
notRunnableReason,
onHelp,
Expand Down Expand Up @@ -74,11 +76,12 @@ const ScriptEditor: FunctionComponent<ScriptEditorProps> = ({

const monacoInstance = useMonaco();

const runCtrlEnter: editor.IActionDescriptor[] = useMemo(() => {
const scriptShortcuts: editor.IActionDescriptor[] = useMemo(() => {
if (!monacoInstance) {
return [];
}
return [
const actions = [
// Ctrl-Enter to run
{
id: "run-script",
label: "Run Script",
Expand All @@ -92,7 +95,19 @@ const ScriptEditor: FunctionComponent<ScriptEditorProps> = ({
},
},
];
}, [monacoInstance, runCode, runnable, unsavedChanges]);
if (onCancel) {
// Ctrl-C to cancel
actions.push({
id: "cancel-script",
label: "Cancel Script",
keybindings: [
monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.KeyC,
],
run: onCancel,
});
}
return actions;
}, [monacoInstance, onCancel, runCode, runnable, unsavedChanges]);

const toolbarItems: ToolbarItem[] = useMemo(() => {
return makeToolbar({
Expand All @@ -101,11 +116,13 @@ const ScriptEditor: FunctionComponent<ScriptEditorProps> = ({
runnable: runnable && !unsavedChanges,
notRunnableReason,
onRun: runCode,
onCancel,
onHelp,
});
}, [
language,
notRunnableReason,
onCancel,
onHelp,
runCode,
runnable,
Expand All @@ -124,7 +141,7 @@ const ScriptEditor: FunctionComponent<ScriptEditorProps> = ({
onSaveText={onSaveText}
toolbarItems={toolbarItems}
contentOnEmpty={contentOnEmpty}
actions={runCtrlEnter}
actions={scriptShortcuts}
/>
<ConsoleOutputPanel consoleRef={consoleRef} />
</Split>
Expand All @@ -138,8 +155,9 @@ const makeToolbar = (o: {
notRunnableReason?: string;
onRun: () => void;
onHelp?: () => void;
onCancel?: () => void;
}): ToolbarItem[] => {
const { status, onRun, runnable, onHelp, name } = o;
const { status, onRun, runnable, onHelp, name, onCancel } = o;
const ret: ToolbarItem[] = [];
if (onHelp !== undefined) {
ret.push({
Expand All @@ -166,6 +184,17 @@ const makeToolbar = (o: {
});
}

if (onCancel && status === "running") {
ret.push({
type: "button",
tooltip: "Cancel",
label: "Cancel",
icon: <Close />,
onClick: onCancel,
color: "error",
});
}

let label: string;
let color: ColorOptions;
if (status === "loading") {
Expand Down
76 changes: 48 additions & 28 deletions gui/src/app/core/Scripting/pyodide/pyodideWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,29 @@ import { isMonacoWorkerNoise } from "@SpUtil/isMonacoWorkerNoise";
import { InterpreterStatus } from "@SpCore/Scripting/InterpreterTypes";
import {
MessageFromPyodideWorker,
MessageToPyodideWorker,
PyodideRunSettings,
} from "./pyodideWorkerTypes";
import spDrawsScript from "./sp_load_draws.py?raw";
import spMPLScript from "./sp_patch_matplotlib.py?raw";

let pyodide: PyodideInterface | null = null;
const loadPyodideInstance = async () => {
if (pyodide === null) {
pyodide = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.27.2/full",
stdout: (x: string) => {
sendStdout(x);
},
stderr: (x: string) => {
sendStderr(x);
},
packages: ["numpy", "micropip", "pandas"],
});
setStatus("installing");

pyodide.FS.writeFile("sp_load_draws.py", spDrawsScript);
pyodide.FS.writeFile("sp_patch_matplotlib.py", spMPLScript);

return pyodide;
} else {
return pyodide;
}
const pyodide = await loadPyodide({
indexURL: "https://cdn.jsdelivr.net/pyodide/v0.27.2/full",
stdout: (x: string) => {
sendStdout(x);
},
stderr: (x: string) => {
sendStderr(x);
},
packages: ["numpy", "micropip", "pandas"],
});
console.log("pyodide loaded");

pyodide.FS.writeFile("sp_load_draws.py", spDrawsScript);
pyodide.FS.writeFile("sp_patch_matplotlib.py", spMPLScript);

return pyodide;
};

const sendMessageToMain = (message: MessageFromPyodideWorker) => {
Expand All @@ -56,25 +52,38 @@ const addImage = (image: any) => {
sendMessageToMain({ type: "addImage", image });
};

console.log("pyodide worker loaded");

self.onmessage = async (e) => {
self.onmessage = async (e: MessageEvent<MessageToPyodideWorker>) => {
if (isMonacoWorkerNoise(e.data)) {
return;
}
const message = e.data;
await run(message.code, message.spData, message.spRunSettings, message.files);
await run(
message.code,
message.spData,
message.spRunSettings,
message.files,
message.interruptBuffer,
);
};
console.log("pyodide worker initialized");

console.log("opportunistically loading pyodide");
const pyodidePromise: Promise<PyodideInterface> = loadPyodideInstance();

const run = async (
code: string,
spData: Record<string, any> | undefined,
spPySettings: PyodideRunSettings,
files: Record<string, string> | undefined,
interruptBuffer: Uint8Array | undefined,
) => {
setStatus("loading");
try {
const pyodide = await loadPyodideInstance();
const pyodide = await pyodidePromise;
if (interruptBuffer) {
pyodide.setInterruptBuffer(interruptBuffer);
}
setStatus("installing");

const [scriptPreamble, scriptPostamble] = getScriptParts(spPySettings);

Expand All @@ -95,6 +104,7 @@ const run = async (
let succeeded = false;
try {
const packageFutures = [];
let patch_http = false;
const micropip = pyodide.pyimport("micropip");

if (spPySettings.showsPlots) {
Expand All @@ -104,10 +114,20 @@ const run = async (
packageFutures.push(micropip.install("arviz"));
}
}
if (script.includes("requests")) {
patch_http = true;
packageFutures.push(
micropip.install(["requests", "lzma", "pyodide-http"]),
);
}
packageFutures.push(micropip.install("stanio"));
packageFutures.push(pyodide.loadPackagesFromImports(script));
for (const f of packageFutures) {
await f;
await Promise.all(packageFutures);
if (patch_http) {
await pyodide.runPythonAsync(`
from pyodide_http import patch_all
patch_all()
`);
}

if (files) {
Expand Down
1 change: 1 addition & 0 deletions gui/src/app/core/Scripting/pyodide/pyodideWorkerTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type MessageToPyodideWorker = {
spData: Record<string, any> | undefined;
spRunSettings: PyodideRunSettings;
files: Record<string, string> | undefined;
interruptBuffer: Uint8Array | undefined;
};

export const isMessageToPyodideWorker = (
Expand Down
Loading