Lite: Show initialization progress messages (#5983)

This commit is contained in:
Yuichiro Tachibana (Tsuchiya) 2023-10-19 02:37:14 +09:00 committed by GitHub
parent d8a6491a18
commit a32aabaf50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 118 additions and 16 deletions

View File

@ -0,0 +1,7 @@
---
"@gradio/app": minor
"@gradio/wasm": minor
"gradio": minor
---
feat:Lite: Show initialization progress messages

View File

@ -94,6 +94,25 @@
export let worker_proxy: WorkerProxy | undefined = undefined;
if (worker_proxy) {
setWorkerProxyContext(worker_proxy);
worker_proxy.addEventListener("progress-update", (event) => {
loading_text = (event as CustomEvent).detail + "...";
});
worker_proxy.addEventListener("initialization-error", (event) => {
const error: Error = (event as CustomEvent).detail;
// XXX: Although `status` is expected to store Space status info,
// we are using it to store the error thrown from the Wasm runtime here
// as a workaround to display the error message in the UI
// without breaking the existing code.
status = {
status: "space_error",
message: error.message,
detail: "RUNTIME_ERROR",
load_status: "error",
discussions_enabled: false
};
});
}
export let space: string | null;

View File

@ -113,3 +113,15 @@ export interface ReplyMessageError {
}
export type ReplyMessage = ReplyMessageSuccess | ReplyMessageError;
export interface OutMessageBase {
type: string;
data: unknown;
}
export interface OutMessageProgressUpdate extends OutMessageBase {
type: "progress-update";
data: {
log: string;
};
}
export type OutMessage = OutMessageProgressUpdate;

View File

@ -5,6 +5,7 @@ import type { PyodideInterface } from "pyodide";
import type {
InMessage,
InMessageInit,
OutMessage,
ReplyMessageError,
ReplyMessageSuccess
} from "../message-types";
@ -29,10 +30,21 @@ let call_asgi_app_from_js: (
let run_script: (path: string) => Promise<void>;
let unload_local_modules: (target_dir_path?: string) => void;
function updateProgress(log: string): void {
const message: OutMessage = {
type: "progress-update",
data: {
log
}
};
self.postMessage(message);
}
async function loadPyodideAndPackages(
options: InMessageInit["data"]
): Promise<void> {
console.debug("Loading Pyodide.");
updateProgress("Loading Pyodide");
pyodide = await loadPyodide({
stdout: console.debug,
stderr: console.error
@ -40,6 +52,7 @@ async function loadPyodideAndPackages(
console.debug("Pyodide is loaded.");
console.debug("Mounting files.", options.files);
updateProgress("Mounting files");
await Promise.all(
Object.keys(options.files).map(async (path) => {
const file = options.files[path];
@ -62,6 +75,7 @@ async function loadPyodideAndPackages(
console.debug("Files are mounted.");
console.debug("Loading micropip");
updateProgress("Loading micropip");
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
console.debug("micropip is loaded.");
@ -71,6 +85,7 @@ async function loadPyodideAndPackages(
options.gradioClientWheelUrl
];
console.debug("Loading Gradio wheels.", gradioWheelUrls);
updateProgress("Loading Gradio wheels");
await micropip.add_mock_package("ffmpy", "0.3.0");
await micropip.add_mock_package("aiohttp", "3.8.4");
await pyodide.loadPackage(["ssl", "distutils", "setuptools"]);
@ -82,11 +97,13 @@ async function loadPyodideAndPackages(
});
console.debug("Gradio wheels are loaded.");
console.debug("Install packages.", options.requirements);
console.debug("Installing packages.", options.requirements);
updateProgress("Installing packages");
await micropip.install.callKwargs(options.requirements, { keep_going: true });
console.debug("Packages are installed.");
console.debug("Mock os module methods.");
console.debug("Mocking os module methods.");
updateProgress("Mock os module methods");
// `os.link` is used in `aiofiles` (https://github.com/Tinche/aiofiles/blob/v23.1.0/src/aiofiles/os.py#L31),
// which is imported from `gradio.ranged_response` (https://github.com/gradio-app/gradio/blob/v3.32.0/gradio/ranged_response.py#L12).
// However, it's not available on Wasm.
@ -97,13 +114,15 @@ os.link = lambda src, dst: None
`);
console.debug("os module methods are mocked.");
console.debug("Import gradio package.");
console.debug("Importing gradio package.");
updateProgress("Importing gradio package");
// Importing the gradio package takes a long time, so we do it separately.
// This is necessary for accurate performance profiling.
await pyodide.runPythonAsync(`import gradio`);
console.debug("gradio package is imported.");
console.debug("Define a ASGI wrapper function.");
console.debug("Defining a ASGI wrapper function.");
updateProgress("Defining a ASGI wrapper function");
// TODO: Unlike Streamlit, user's code is executed in the global scope,
// so we should not define this function in the global scope.
await pyodide.runPythonAsync(`
@ -143,7 +162,8 @@ async def _call_asgi_app_from_js(scope, receive, send):
call_asgi_app_from_js = pyodide.globals.get("_call_asgi_app_from_js");
console.debug("The ASGI wrapper function is defined.");
console.debug("Mock async libraries.");
console.debug("Mocking async libraries.");
updateProgress("Mocking async libraries");
// FastAPI uses `anyio.to_thread.run_sync` internally which, however, doesn't work in Wasm environments where the `threading` module is not supported.
// So we mock `anyio.to_thread.run_sync` here not to use threads.
await pyodide.runPythonAsync(`
@ -155,7 +175,8 @@ anyio.to_thread.run_sync = mocked_anyio_to_thread_run_sync
`);
console.debug("Async libraries are mocked.");
console.debug("Set matplotlib backend.");
console.debug("Setting matplotlib backend.");
updateProgress("Setting matplotlib backend");
// Ref: https://github.com/streamlit/streamlit/blob/1.22.0/lib/streamlit/web/bootstrap.py#L111
// This backend setting is required to use matplotlib in Wasm environment.
await pyodide.runPythonAsync(`
@ -164,7 +185,8 @@ matplotlib.use("agg")
`);
console.debug("matplotlib backend is set.");
console.debug("Set up Python utility functions.");
console.debug("Setting up Python utility functions.");
updateProgress("Setting up Python utility functions");
await pyodide.runPythonAsync(scriptRunnerPySource);
run_script = pyodide.globals.get("_run_script");
await pyodide.runPythonAsync(unloadModulesPySource);
@ -182,11 +204,21 @@ self.onmessage = async (event: MessageEvent<InMessage>): Promise<void> => {
if (msg.type === "init") {
pyodideReadyPromise = loadPyodideAndPackages(msg.data);
const replyMessage: ReplyMessageSuccess = {
type: "reply:success",
data: null
};
messagePort.postMessage(replyMessage);
pyodideReadyPromise
.then(() => {
const replyMessage: ReplyMessageSuccess = {
type: "reply:success",
data: null
};
messagePort.postMessage(replyMessage);
})
.catch((error) => {
const replyMessage: ReplyMessageError = {
type: "reply:error",
error
};
messagePort.postMessage(replyMessage);
});
return;
}

View File

@ -6,6 +6,7 @@ import type {
HttpResponse,
InMessage,
InMessageWebSocket,
OutMessage,
ReplyMessage
} from "./message-types";
import { MessagePortWebSocket } from "./messageportwebsocket";
@ -18,12 +19,14 @@ export interface WorkerProxyOptions {
requirements: string[];
}
export class WorkerProxy {
export class WorkerProxy extends EventTarget {
private worker: globalThis.Worker;
private firstRunPromiseDelegate = new PromiseDelegate<void>();
constructor(options: WorkerProxyOptions) {
super();
console.debug("WorkerProxy.constructor(): Create a new worker.");
// Loading a worker here relies on Vite's support for WebWorkers (https://vitejs.dev/guide/features.html#web-workers),
// assuming that this module is imported from the Gradio frontend (`@gradio/app`), which is bundled with Vite.
@ -32,6 +35,10 @@ export class WorkerProxy {
const workerMaker = new Worker(new URL("./webworker.js", import.meta.url));
this.worker = workerMaker.worker;
this.worker.onmessage = (e) => {
this._processWorkerMessage(e.data);
};
this.postMessageAsync({
type: "init",
data: {
@ -40,9 +47,21 @@ export class WorkerProxy {
files: options.files,
requirements: options.requirements
}
}).then(() => {
console.debug("WorkerProxy.constructor(): Initialization is done.");
});
})
.then(() => {
console.debug("WorkerProxy.constructor(): Initialization is done.");
})
.catch((error) => {
console.error(
"WorkerProxy.constructor(): Initialization failed.",
error
);
this.dispatchEvent(
new CustomEvent("initialization-error", {
detail: error
})
);
});
}
public async runPythonCode(code: string): Promise<void> {
@ -88,6 +107,19 @@ export class WorkerProxy {
});
}
private _processWorkerMessage(msg: OutMessage): void {
switch (msg.type) {
case "progress-update": {
this.dispatchEvent(
new CustomEvent("progress-update", {
detail: msg.data.log
})
);
break;
}
}
}
public async httpRequest(request: HttpRequest): Promise<HttpResponse> {
// Wait for the first run to be done
// to avoid the "Gradio app has not been launched." error