perf: speed up startup from ~15s to ~2s (#280)

Three high-impact changes eliminate most startup latency:

1. Enable wasmtime persistent compilation cache — call
   cache_config_load_default() so compiled native code is serialized to
   disk (~/.cache/wasmtime). Subsequent startups deserialize instead of
   recompiling, dropping the WASM phase from ~13s to <1s.

2. Cache compiled Component in PreparedModule — store the compiled
   wasmtime::component::Component directly instead of raw bytes.
   Eliminates ~2.6s recompilation on every first tool/channel execution.

3. Move blocking housekeeping to background tasks — embedding backfill
   (~1.3s of failing HTTP calls) and stale job cleanup are fire-and-forget
   work that no longer blocks the critical startup path.

Also: deduplicate Workspace creation in main.rs (two identical instances
reduced to one), and replace leftover println! in session validation with
tracing calls.

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-21 02:30:57 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 2cdd1acb1e
commit 98ee648fcb
8 changed files with 130 additions and 81 deletions
+1 -1
View File
@@ -488,7 +488,7 @@ mod tests {
let prepared = Arc::new(PreparedChannelModule {
name: name.to_string(),
description: format!("Test channel: {}", name),
component_bytes: Vec::new(),
component: None,
limits: ResourceLimits::default(),
});
+30 -10
View File
@@ -68,38 +68,51 @@ impl WasmChannelRuntimeConfig {
}
/// A compiled WASM channel component ready for instantiation.
#[derive(Debug)]
///
/// Stores the pre-compiled `Component` directly so instantiation
/// doesn't require recompilation.
pub struct PreparedChannelModule {
/// Channel name.
pub name: String,
/// Channel description.
pub description: String,
/// Compiled component bytes (public for testing, otherwise use component_bytes()).
pub(crate) component_bytes: Vec<u8>,
/// Pre-compiled component (cheaply cloneable via internal Arc).
pub(crate) component: Option<wasmtime::component::Component>,
/// Resource limits for this channel.
pub limits: ResourceLimits,
}
impl PreparedChannelModule {
/// Get the compiled component bytes.
pub fn component_bytes(&self) -> &[u8] {
&self.component_bytes
/// Get the pre-compiled component for instantiation.
pub fn component(&self) -> Option<&wasmtime::component::Component> {
self.component.as_ref()
}
/// Create a PreparedChannelModule for testing purposes.
///
/// Creates a module with no actual WASM bytes, suitable for testing
/// Creates a module with no actual WASM component, suitable for testing
/// channel infrastructure without requiring a real WASM component.
pub fn for_testing(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
component_bytes: Vec::new(),
component: None,
limits: ResourceLimits::default(),
}
}
}
impl std::fmt::Debug for PreparedChannelModule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreparedChannelModule")
.field("name", &self.name)
.field("description", &self.description)
.field("has_component", &self.component.is_some())
.field("limits", &self.limits)
.finish()
}
}
/// WASM channel runtime.
///
/// Manages the Wasmtime engine and a cache of prepared channel modules.
@@ -137,6 +150,13 @@ impl WasmChannelRuntime {
// Disable debug info in production
wasmtime_config.debug_info(false);
// Enable persistent compilation cache. Wasmtime serializes compiled native
// code to disk (~/.cache/wasmtime by default), so subsequent startups
// deserialize instead of recompiling — typically 10-50x faster.
if let Err(e) = wasmtime_config.cache_config_load_default() {
tracing::warn!("Failed to enable wasmtime compilation cache: {}", e);
}
let engine = Engine::new(&wasmtime_config).map_err(|e| {
WasmChannelError::Config(format!("Failed to create Wasmtime engine: {}", e))
})?;
@@ -183,13 +203,13 @@ impl WasmChannelRuntime {
// Compile in blocking task (Wasmtime compilation is synchronous)
let prepared = tokio::task::spawn_blocking(move || {
// Validate and compile the component
let _component = wasmtime::component::Component::new(&engine, &wasm_bytes)
let component = wasmtime::component::Component::new(&engine, &wasm_bytes)
.map_err(|e| WasmChannelError::Compilation(e.to_string()))?;
Ok::<_, WasmChannelError>(PreparedChannelModule {
name: name.clone(),
description: desc,
component_bytes: wasm_bytes,
component: Some(component),
limits: limits.unwrap_or(default_limits),
})
})
+19 -15
View File
@@ -37,7 +37,7 @@ use tokio::sync::{RwLock, mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use uuid::Uuid;
use wasmtime::Store;
use wasmtime::component::{Component, Linker};
use wasmtime::component::Linker;
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiView};
use crate::channels::wasm::capabilities::ChannelCapabilities;
@@ -725,9 +725,13 @@ impl WasmChannel {
) -> Result<SandboxedChannel, WasmChannelError> {
let engine = runtime.engine();
// Compile the component (uses cached bytes)
let component = Component::new(engine, prepared.component_bytes())
.map_err(|e| WasmChannelError::Compilation(e.to_string()))?;
// Use the pre-compiled component (no recompilation needed)
let component = prepared
.component()
.ok_or_else(|| {
WasmChannelError::Compilation("No compiled component available".to_string())
})?
.clone();
// Create linker and add host functions
let mut linker = Linker::new(engine);
@@ -778,7 +782,7 @@ impl WasmChannel {
/// Returns the channel configuration for HTTP endpoint registration.
async fn call_on_start(&self) -> Result<ChannelConfig, WasmChannelError> {
// If no WASM bytes, return default config (for testing)
if self.prepared.component_bytes.is_empty() {
if self.prepared.component().is_none() {
tracing::info!(
channel = %self.name,
"WASM channel on_start called (no WASM module, returning defaults)"
@@ -918,7 +922,7 @@ impl WasmChannel {
);
// If no WASM bytes, return 200 OK (for testing)
if self.prepared.component_bytes.is_empty() {
if self.prepared.component().is_none() {
tracing::debug!(
channel = %self.name,
method = method,
@@ -1018,7 +1022,7 @@ impl WasmChannel {
/// Called periodically if polling is configured.
pub async fn call_on_poll(&self) -> Result<(), WasmChannelError> {
// If no WASM bytes, do nothing (for testing)
if self.prepared.component_bytes.is_empty() {
if self.prepared.component().is_none() {
tracing::debug!(
channel = %self.name,
"WASM channel on_poll called (no WASM module)"
@@ -1118,7 +1122,7 @@ impl WasmChannel {
);
// If no WASM bytes, do nothing (for testing)
if self.prepared.component_bytes.is_empty() {
if self.prepared.component().is_none() {
tracing::debug!(
channel = %self.name,
message_id = %message_id,
@@ -1236,7 +1240,7 @@ impl WasmChannel {
metadata: &serde_json::Value,
) -> Result<(), WasmChannelError> {
// If no WASM bytes, do nothing (for testing)
if self.prepared.component_bytes.is_empty() {
if self.prepared.component().is_none() {
return Ok(());
}
@@ -1307,7 +1311,7 @@ impl WasmChannel {
timeout: Duration,
wit_update: wit_channel::StatusUpdate,
) -> Result<(), WasmChannelError> {
if prepared.component_bytes.is_empty() {
if prepared.component().is_none() {
return Ok(());
}
@@ -1627,7 +1631,7 @@ impl WasmChannel {
workspace_store: &Arc<ChannelWorkspaceStore>,
) -> Result<Vec<EmittedMessage>, WasmChannelError> {
// Skip if no WASM bytes (testing mode)
if prepared.component_bytes.is_empty() {
if prepared.component().is_none() {
tracing::debug!(
channel = %channel_name,
"WASM channel on_poll called (no WASM module)"
@@ -2206,7 +2210,7 @@ mod tests {
let prepared = Arc::new(PreparedChannelModule {
name: "test".to_string(),
description: "Test channel".to_string(),
component_bytes: Vec::new(),
component: None,
limits: ResourceLimits::default(),
});
@@ -2271,7 +2275,7 @@ mod tests {
#[tokio::test]
async fn test_execute_poll_no_wasm_returns_empty() {
// When there's no WASM module (empty component_bytes), execute_poll
// When there's no WASM module (None component), execute_poll
// should return an empty vector of messages
let config = WasmChannelRuntimeConfig::for_testing();
let runtime = Arc::new(WasmChannelRuntime::new(config).unwrap());
@@ -2279,7 +2283,7 @@ mod tests {
let prepared = Arc::new(PreparedChannelModule {
name: "poll-test".to_string(),
description: "Test channel".to_string(),
component_bytes: Vec::new(), // No WASM bytes
component: None, // No WASM module
limits: ResourceLimits::default(),
});
@@ -2381,7 +2385,7 @@ mod tests {
let prepared = Arc::new(PreparedChannelModule {
name: "poll-channel".to_string(),
description: "Polling test channel".to_string(),
component_bytes: Vec::new(),
component: None,
limits: ResourceLimits::default(),
});