diff --git a/Cargo.lock b/Cargo.lock index a5f1d0e3e16..c22833198b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1226,6 +1226,17 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-macro" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.92", +] + [[package]] name = "futures-sink" version = "0.3.27" @@ -1245,10 +1256,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" dependencies = [ "futures-core", + "futures-macro", "futures-sink", "futures-task", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -3358,6 +3371,7 @@ dependencies = [ "criterion", "env_logger", "filecheck", + "futures-util", "http", "http-body-util", "humantime", @@ -3377,6 +3391,7 @@ dependencies = [ "tempfile", "test-programs-artifacts", "tokio", + "tokio-util", "tracing", "walkdir", "wasmparser 0.120.0", diff --git a/Cargo.toml b/Cargo.toml index 2bffa7bcdf2..dbd628a974f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,9 @@ humantime = { workspace = true } async-trait = { workspace = true } bytes = { workspace = true } -tokio = { workspace = true, optional = true, features = [ "signal", "macros" ] } +futures-util = { version = "*", optional = true } +tokio = { workspace = true, optional = true, features = [ "fs", "signal", "macros" ] } +tokio-util = { version = "*", optional = true } hyper = { workspace = true, optional = true } http = { workspace = true, optional = true } http-body-util = { workspace = true, optional = true } @@ -369,7 +371,7 @@ parallel-compilation = ["wasmtime-cli-flags/parallel-compilation"] logging = ["wasmtime-cli-flags/logging"] demangle = ["wasmtime/demangle"] cranelift = ["wasmtime-cli-flags/cranelift", "dep:wasmtime-cranelift"] -profiling = ["wasmtime/profiling"] +profiling = ["wasmtime/profiling", "dep:futures-util", "dep:hyper", "dep:tokio-util"] coredump = ["wasmtime-cli-flags/coredump"] addr2line = ["wasmtime/addr2line"] debug-builtins = ["wasmtime/debug-builtins"] diff --git a/src/commands/run.rs b/src/commands/run.rs index 1fdf32a954a..d96c174b867 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -8,15 +8,27 @@ use crate::common::{Profile, RunCommon, RunTarget}; use anyhow::{anyhow, bail, Context as _, Error, Result}; +use bytes::Bytes; use clap::Parser; +use futures_util::stream::MapOk; +use futures_util::TryStreamExt; +use http::{Response, StatusCode}; +use http_body_util::StreamBody; +use hyper::body::Frame; +use hyper::server::conn::http1; +use hyper::service::service_fn; use std::ffi::OsString; +use std::net::{Ipv4Addr, SocketAddrV4}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread; +use tokio::fs::File; +use tokio_util::io::ReaderStream; use wasmtime::{Engine, Func, Module, Store, StoreLimits, Val, ValType}; use wasmtime_wasi::maybe_exit_on_error; use wasmtime_wasi::preview2; use wasmtime_wasi::sync::{ambient_authority, Dir, TcpListener, WasiCtxBuilder}; +use wasmtime_wasi_http::io::TokioIo; #[cfg(feature = "wasi-nn")] use wasmtime_wasi_nn::WasiNnCtx; @@ -370,11 +382,78 @@ impl RunCommand { } else { eprintln!(); eprintln!("Profile written to: {path}"); - eprintln!("View this profile at https://profiler.firefox.com/."); + if let Err(e) = Self::run_profile_server(path) { + eprintln!("Local profile server failed: {e:#}"); + eprintln!("View this profile at https://profiler.firefox.com/."); + } } }); } + fn run_profile_server(path: String) -> anyhow::Result<()> { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_io() + .build()?; + + runtime.block_on(async move { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + Ok(()) + } + + res = Self::serve_profile(path) => { + res + } + } + }) + } + + async fn serve_profile(path: String) -> anyhow::Result<()> { + let sockaddr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0); + let listener = tokio::net::TcpListener::bind(sockaddr).await?; + // FIXME: properly url-encode the from-url + eprintln!( + "View this profile at https://profiler.firefox.com/from-url/http:%2F%2F{}/", + listener.local_addr()? + ); + let path = Arc::new(path); + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + let path = path.clone(); + tokio::task::spawn(async move { + let service = service_fn(|_req| Self::serve_file(&path)); + if let Err(e) = http1::Builder::new() + .keep_alive(true) + .serve_connection(io, service) + .await + { + eprintln!("incoming connection error: {e:?}"); + } + }); + } + } + + async fn serve_file( + path: &str, + ) -> anyhow::Result< + Response, impl Fn(Bytes) -> Frame>>>>, + > { + let file = File::open(path).await?; + let reader_stream = tokio_util::io::ReaderStream::new(file); + let stream_body = Box::new(StreamBody::new(reader_stream.map_ok(Frame::data))); + let mut response = Response::builder() + .status(StatusCode::OK) + .body(stream_body) + .unwrap(); + // FIXME: only offer CORS header if a XSRF token is present in the request path + response.headers_mut().insert( + http::header::ACCESS_CONTROL_ALLOW_ORIGIN, + http::header::HeaderValue::from_static("*"), + ); + Ok(response) + } + fn load_main_module( &self, store: &mut Store,