Skip to content

Commit e026f3c

Browse files
committed
feat: add V8 code cache with in-memory LRU
Replace the old snapshot-based caching with V8 code cache: - On first execution, transpile and run the worker code - In the background, create a code cache (compiled bytecode) - On subsequent requests, load from in-memory LRU (5000 entries) Code cache bundles embed source + bytecode in a simple wire format: [magic 0xC0DECA5E][source_len][source][cache_bytes]
1 parent 140642c commit e026f3c

6 files changed

Lines changed: 101 additions & 42 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ tokio = "1.49.0"
3737
tokio-util = { version = "0.7", features = ["rt"] }
3838
flume = "0.12.0"
3939
once_cell = "1.19"
40+
# filetime = "0.2" # was used by disk-based snapshot cache
4041
env_logger = "0.11.6"
4142
lru = "0.16.3"
4243

@@ -62,7 +63,7 @@ openworkers-core = { git = "https://github.com/openworkers/openworkers-core", ta
6263
openworkers-transform = { git = "https://github.com/openworkers/openworkers-transform", tag = "v0.1.0" }
6364

6465
# Runtime backend (v8 only for now, others require older version of core)
65-
openworkers-runtime-v8 = { git = "https://github.com/openworkers/openworkers-runtime-v8", tag = "v0.13.2", optional = true, features = ["ptrcomp"] }
66+
openworkers-runtime-v8 = { path = "../openworkers-runtime-v8", optional = true, features = ["ptrcomp"] }
6667

6768
# WASM runtime (optional)
6869
# openworkers-runtime-wasm = { path = "../openworkers-runtime-wasm", optional = true }

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub mod nats;
1111
pub mod ops;
1212
pub mod runtime;
1313
pub mod services;
14+
#[cfg(feature = "v8")]
15+
pub mod snapshot_cache;
1416
pub mod store;
1517
pub mod task_executor;
1618
pub mod telemetry;

src/snapshot_cache.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! In-memory LRU cache for worker snapshots.
2+
//!
3+
//! Stores V8 snapshot blobs in memory, keyed by `(worker_id, version)`.
4+
//! This avoids disk I/O on every request and keeps snapshot bytes readily
5+
//! available for V8 isolate creation.
6+
7+
use std::num::NonZeroUsize;
8+
use std::sync::Mutex;
9+
10+
use lru::LruCache;
11+
use once_cell::sync::Lazy;
12+
13+
/// Default max number of cached snapshots.
14+
const DEFAULT_MAX_ENTRIES: usize = 500;
15+
16+
/// Cache key: (worker_id, version)
17+
type CacheKey = (String, i32);
18+
19+
/// Global in-memory LRU cache for worker snapshots.
20+
static SNAPSHOT_CACHE: Lazy<Mutex<LruCache<CacheKey, Vec<u8>>>> = Lazy::new(|| {
21+
let max_entries = std::env::var("SNAPSHOT_CACHE_MAX")
22+
.ok()
23+
.and_then(|v| v.parse().ok())
24+
.unwrap_or(DEFAULT_MAX_ENTRIES);
25+
26+
tracing::info!("Snapshot cache: in-memory, max_entries={}", max_entries);
27+
28+
Mutex::new(LruCache::new(
29+
NonZeroUsize::new(max_entries).unwrap_or(NonZeroUsize::new(DEFAULT_MAX_ENTRIES).unwrap()),
30+
))
31+
});
32+
33+
/// Try to read a cached worker snapshot from memory.
34+
pub fn get(worker_id: &str, version: i32) -> Option<Vec<u8>> {
35+
let key = (worker_id.to_string(), version);
36+
let mut cache = SNAPSHOT_CACHE.lock().unwrap();
37+
cache.get(&key).cloned()
38+
}
39+
40+
/// Store a worker snapshot in memory.
41+
pub fn put(worker_id: &str, version: i32, snapshot: &[u8]) {
42+
let key = (worker_id.to_string(), version);
43+
let mut cache = SNAPSHOT_CACHE.lock().unwrap();
44+
cache.put(key, snapshot.to_vec());
45+
}

src/task_executor.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@ pub async fn execute_task_await_v8_pooled(
169169
let components = prepare_task_components(&config)
170170
.ok_or_else(|| TerminationReason::Other("Failed to prepare script".to_string()))?;
171171

172+
// Capture JS code for background code cache creation (before script is moved)
173+
let js_code_for_snapshot = components.script.code.as_js().map(|s| s.to_string());
174+
let worker_id_for_snapshot = config.worker_data.id.clone();
175+
let version_for_snapshot = config.worker_data.version;
176+
172177
// Use user_id (tenant) for isolate pool isolation instead of worker_id
173178
// This prevents a single tenant from monopolizing resources via multiple workers
174179
let owner_id = config.worker_data.user_id.clone();
@@ -223,6 +228,37 @@ pub async fn execute_task_await_v8_pooled(
223228
// CRITICAL: Flush logs before returning
224229
components.log_handler.flush();
225230

231+
// After successful JS execution, create code cache in background
232+
if result.is_ok()
233+
&& let Some(js_code) = js_code_for_snapshot
234+
{
235+
let worker_id = worker_id_for_snapshot;
236+
let version = version_for_snapshot;
237+
238+
tokio::task::spawn_blocking(move || {
239+
match openworkers_runtime_v8::create_code_cache(&js_code) {
240+
Ok(cache) => {
241+
let packed =
242+
openworkers_runtime_v8::pack_code_cache(&js_code, &cache);
243+
crate::snapshot_cache::put(&worker_id, version, &packed);
244+
tracing::debug!(
245+
"Created code cache: worker={}, version={}, size={}",
246+
crate::utils::short_id(&worker_id),
247+
version,
248+
packed.len()
249+
);
250+
}
251+
Err(e) => {
252+
tracing::warn!(
253+
"Failed to create code cache for worker={}: {}",
254+
crate::utils::short_id(&worker_id),
255+
e
256+
);
257+
}
258+
}
259+
});
260+
}
261+
226262
result
227263
}
228264
.instrument(span)

src/worker.rs

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,11 @@
55
//! - wasm only: direct type alias to WasmWorker (zero overhead)
66
//! - both: enum with runtime dispatch based on CodeType
77
8-
use std::num::NonZeroUsize;
9-
use std::sync::{Arc, Mutex};
10-
11-
use lru::LruCache;
12-
use once_cell::sync::Lazy;
8+
use std::sync::Arc;
139

1410
use crate::ops::RunnerOperations;
1511
use crate::store::{CodeType, WorkerWithBindings, bindings_to_infos};
1612

17-
// =============================================================================
18-
// Transpiled code cache
19-
// =============================================================================
20-
21-
/// Cache key: (worker_id, version)
22-
type CacheKey = (String, i32);
23-
24-
/// LRU cache for transpiled JavaScript code
25-
/// Capacity: 1000 workers (transpiled code is relatively small)
26-
static TRANSPILE_CACHE: Lazy<Mutex<LruCache<CacheKey, String>>> =
27-
Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(1000).unwrap())));
28-
2913
#[cfg(all(feature = "v8", feature = "wasm"))]
3014
use openworkers_core::Event;
3115

@@ -113,35 +97,33 @@ pub async fn create_worker(
11397
// Script preparation (shared across all configurations)
11498
// =============================================================================
11599

116-
/// Parse worker code based on code type (with caching for JS/TS)
100+
/// Parse worker code based on code type.
101+
///
102+
/// For JS/TS workers: checks the code cache (fast path) and returns cached
103+
/// bytecode. On cache miss, transpiles the source and returns JS — a code
104+
/// cache entry is created in the background after first successful execution.
117105
fn parse_code(data: &WorkerWithBindings) -> Result<WorkerCode, TerminationReason> {
118106
match data.code_type {
119107
CodeType::Javascript | CodeType::Typescript => {
120108
#[cfg(feature = "v8")]
121109
{
122-
let cache_key = (data.id.clone(), data.version);
123-
124-
// Try to get from cache first
125-
{
126-
let mut cache = TRANSPILE_CACHE.lock().unwrap();
127-
128-
if let Some(cached_code) = cache.get(&cache_key) {
129-
tracing::debug!(
130-
"transpile cache HIT: worker={}, version={}",
131-
crate::utils::short_id(&data.id),
132-
data.version
133-
);
134-
return Ok(WorkerCode::js(cached_code.clone()));
135-
}
110+
// Fast path: check code cache
111+
if let Some(snapshot) = crate::snapshot_cache::get(&data.id, data.version) {
112+
tracing::debug!(
113+
"code cache HIT: worker={}, version={}",
114+
crate::utils::short_id(&data.id),
115+
data.version
116+
);
117+
return Ok(WorkerCode::snapshot(snapshot));
136118
}
137119

138120
tracing::debug!(
139-
"transpile cache MISS: worker={}, version={}",
121+
"code cache MISS: worker={}, version={}",
140122
crate::utils::short_id(&data.id),
141123
data.version
142124
);
143125

144-
// Transpile and cache
126+
// Slow path: transpile code (code cache created after first execution)
145127
let language = match data.code_type {
146128
CodeType::Javascript => openworkers_transform::CodeLanguage::JavaScript,
147129
CodeType::Typescript => openworkers_transform::CodeLanguage::TypeScript,
@@ -156,11 +138,6 @@ fn parse_code(data: &WorkerWithBindings) -> Result<WorkerCode, TerminationReason
156138
))
157139
})?;
158140

159-
{
160-
let mut cache = TRANSPILE_CACHE.lock().unwrap();
161-
cache.put(cache_key, transpiled.clone());
162-
}
163-
164141
Ok(WorkerCode::js(transpiled))
165142
}
166143

0 commit comments

Comments
 (0)