Skip to content

Commit 4ece4a6

Browse files
authored
extension_host: Use wasmtime incremental compilation (#30948)
Builds on top of #30942 This turns on incremental compilation and decreases extension compilation times by up to another 41% Putting us at roughly 92% improved extension load times from what is in the app today. Because we only have a static engine, I can't reset the cache between every run. So technically the benchmarks are always running with a warmed cache. So the first extension we load will take the 8.8ms, and then any subsequent extensions will be closer to the measured time in this benchmark. This is also measuring the entire load process, not just the compilation. However, since this is the loading we likely think of when thinking about extensions, I felt it was likely more helpful to see the impact on the overall time. This works because our extensions are largely the same Wasm bytecode (SDK code + std lib functions etc) with minor changes in the trait impl. The more different that extensions implementation is, there will be less benefit, however, there will always be a large part of every extension that is always the same across extensions, so this should be a speedup regardless. I used `moka` to provide a bound to the cache. We could use a bare `DashMap`, however if there was some issue this could lead to a memory leak. `moka` has some slight overhead, but makes sure that we don't go over 32mb while using an LRU-style mechanism for deciding which compilation artifacts to keep. I measured our current extensions to take roughly 512kb in the cache. Which means with a cap of 32mb, we can keep roughly 64 *completely novel* extensions with no overlap. Since our extensions will have more overlap than this though, we can actually keep much more in the cache without having to worry about it. #### Before: ``` load/1 time: [8.8301 ms 8.8616 ms 8.8931 ms] change: [-0.1880% +0.3221% +0.8679%] (p = 0.23 > 0.05) No change in performance detected. ``` #### After: ``` load/1 time: [5.1575 ms 5.1726 ms 5.1876 ms] change: [-41.894% -41.628% -41.350%] (p = 0.00 < 0.05) Performance has improved. ``` Release Notes: - N/A
1 parent 77c2aec commit 4ece4a6

File tree

5 files changed

+224
-16
lines changed

5 files changed

+224
-16
lines changed

Cargo.lock

Lines changed: 60 additions & 0 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
476476
markup5ever_rcdom = "0.3.0"
477477
metal = "0.29"
478478
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
479+
moka = { version = "0.12.10", features = ["sync"] }
479480
naga = { version = "25.0", features = ["wgsl-in"] }
480481
nanoid = "0.4"
481482
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -609,6 +610,7 @@ wasmtime = { version = "29", default-features = false, features = [
609610
"runtime",
610611
"cranelift",
611612
"component-model",
613+
"incremental-cache",
612614
"parallel-compilation",
613615
] }
614616
wasmtime-wasi = "29"

crates/extension_host/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ http_client.workspace = true
3131
language.workspace = true
3232
log.workspace = true
3333
lsp.workspace = true
34+
moka.workspace = true
3435
node_runtime.workspace = true
3536
paths.workspace = true
3637
project.workspace = true

crates/extension_host/src/wasm_host.rs

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ use gpui::{App, AsyncApp, BackgroundExecutor, Task};
2222
use http_client::HttpClient;
2323
use language::LanguageName;
2424
use lsp::LanguageServerName;
25+
use moka::sync::Cache;
2526
use node_runtime::NodeRuntime;
2627
use release_channel::ReleaseChannel;
2728
use semantic_version::SemanticVersion;
29+
use std::borrow::Cow;
30+
use std::sync::LazyLock;
2831
use std::{
2932
path::{Path, PathBuf},
30-
sync::{Arc, OnceLock},
33+
sync::Arc,
3134
};
3235
use wasmtime::{
33-
Engine, Store,
36+
CacheStore, Engine, Store,
3437
component::{Component, ResourceTable},
3538
};
3639
use wasmtime_wasi::{self as wasi, WasiView};
@@ -411,16 +414,23 @@ type ExtensionCall = Box<
411414
>;
412415

413416
fn wasm_engine() -> wasmtime::Engine {
414-
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
415-
416-
WASM_ENGINE
417-
.get_or_init(|| {
418-
let mut config = wasmtime::Config::new();
419-
config.wasm_component_model(true);
420-
config.async_support(true);
421-
wasmtime::Engine::new(&config).unwrap()
422-
})
423-
.clone()
417+
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
418+
let mut config = wasmtime::Config::new();
419+
config.wasm_component_model(true);
420+
config.async_support(true);
421+
config
422+
.enable_incremental_compilation(cache_store())
423+
.unwrap();
424+
wasmtime::Engine::new(&config).unwrap()
425+
});
426+
427+
WASM_ENGINE.clone()
428+
}
429+
430+
fn cache_store() -> Arc<IncrementalCompilationCache> {
431+
static CACHE_STORE: LazyLock<Arc<IncrementalCompilationCache>> =
432+
LazyLock::new(|| Arc::new(IncrementalCompilationCache::new()));
433+
CACHE_STORE.clone()
424434
}
425435

426436
impl WasmHost {
@@ -667,3 +677,132 @@ impl wasi::WasiView for WasmState {
667677
&mut self.ctx
668678
}
669679
}
680+
681+
/// Wrapper around a mini-moka bounded cache for storing incremental compilation artifacts.
682+
/// Since wasm modules have many similar elements, this can save us a lot of work at the
683+
/// cost of a small memory footprint. However, we don't want this to be unbounded, so we use
684+
/// a LFU/LRU cache to evict less used cache entries.
685+
#[derive(Debug)]
686+
struct IncrementalCompilationCache {
687+
cache: Cache<Vec<u8>, Vec<u8>>,
688+
}
689+
690+
impl IncrementalCompilationCache {
691+
fn new() -> Self {
692+
let cache = Cache::builder()
693+
// Cap this at 32 MB for now. Our extensions turn into roughly 512kb in the cache,
694+
// which means we could store 64 completely novel extensions in the cache, but in
695+
// practice we will more than that, which is more than enough for our use case.
696+
.max_capacity(32 * 1024 * 1024)
697+
.weigher(|k: &Vec<u8>, v: &Vec<u8>| (k.len() + v.len()).try_into().unwrap_or(u32::MAX))
698+
.build();
699+
Self { cache }
700+
}
701+
}
702+
703+
impl CacheStore for IncrementalCompilationCache {
704+
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
705+
self.cache.get(key).map(|v| v.into())
706+
}
707+
708+
fn insert(&self, key: &[u8], value: Vec<u8>) -> bool {
709+
self.cache.insert(key.to_vec(), value);
710+
true
711+
}
712+
}
713+
714+
#[cfg(test)]
715+
mod tests {
716+
use std::collections::BTreeMap;
717+
718+
use extension::{
719+
ExtensionCapability, ExtensionLibraryKind, LanguageServerManifestEntry, LibManifestEntry,
720+
SchemaVersion,
721+
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
722+
};
723+
use gpui::TestAppContext;
724+
use reqwest_client::ReqwestClient;
725+
726+
use super::*;
727+
728+
#[gpui::test]
729+
fn test_cache_size_for_test_extension(cx: &TestAppContext) {
730+
let cache_store = cache_store();
731+
let engine = wasm_engine();
732+
let wasm_bytes = wasm_bytes(cx, &mut manifest());
733+
734+
Component::new(&engine, wasm_bytes).unwrap();
735+
736+
cache_store.cache.run_pending_tasks();
737+
let size: usize = cache_store
738+
.cache
739+
.iter()
740+
.map(|(k, v)| k.len() + v.len())
741+
.sum();
742+
// If this assertion fails, it means extensions got larger and we may want to
743+
// reconsider our cache size.
744+
assert!(size < 512 * 1024);
745+
}
746+
747+
fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest) -> Vec<u8> {
748+
let extension_builder = extension_builder();
749+
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
750+
.parent()
751+
.unwrap()
752+
.parent()
753+
.unwrap()
754+
.join("extensions/test-extension");
755+
cx.executor()
756+
.block(extension_builder.compile_extension(
757+
&path,
758+
manifest,
759+
CompileExtensionOptions { release: true },
760+
))
761+
.unwrap();
762+
std::fs::read(path.join("extension.wasm")).unwrap()
763+
}
764+
765+
fn extension_builder() -> ExtensionBuilder {
766+
let user_agent = format!(
767+
"Zed Extension CLI/{} ({}; {})",
768+
env!("CARGO_PKG_VERSION"),
769+
std::env::consts::OS,
770+
std::env::consts::ARCH
771+
);
772+
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap());
773+
// Local dir so that we don't have to download it on every run
774+
let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build");
775+
ExtensionBuilder::new(http_client, build_dir)
776+
}
777+
778+
fn manifest() -> ExtensionManifest {
779+
ExtensionManifest {
780+
id: "test-extension".into(),
781+
name: "Test Extension".into(),
782+
version: "0.1.0".into(),
783+
schema_version: SchemaVersion(1),
784+
description: Some("An extension for use in tests.".into()),
785+
authors: Vec::new(),
786+
repository: None,
787+
themes: Default::default(),
788+
icon_themes: Vec::new(),
789+
lib: LibManifestEntry {
790+
kind: Some(ExtensionLibraryKind::Rust),
791+
version: Some(SemanticVersion::new(0, 1, 0)),
792+
},
793+
languages: Vec::new(),
794+
grammars: BTreeMap::default(),
795+
language_servers: [("gleam".into(), LanguageServerManifestEntry::default())]
796+
.into_iter()
797+
.collect(),
798+
context_servers: BTreeMap::default(),
799+
slash_commands: BTreeMap::default(),
800+
indexed_docs_providers: BTreeMap::default(),
801+
snippets: None,
802+
capabilities: vec![ExtensionCapability::ProcessExec {
803+
command: "echo".into(),
804+
args: vec!["hello!".into()],
805+
}],
806+
}
807+
}
808+
}

tooling/workspace-hack/Cargo.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ chrono = { version = "0.4", features = ["serde"] }
4444
clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
4545
clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
4646
concurrent-queue = { version = "2" }
47+
cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
4748
crc32fast = { version = "1" }
49+
crossbeam-epoch = { version = "0.9" }
4850
crossbeam-utils = { version = "0.8" }
4951
deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
5052
digest = { version = "0.10", features = ["mac", "oid", "std"] }
@@ -95,6 +97,7 @@ prost-types = { version = "0.9" }
9597
rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
9698
rand_chacha = { version = "0.3" }
9799
rand_core = { version = "0.6", default-features = false, features = ["std"] }
100+
regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
98101
regex = { version = "1" }
99102
regex-automata = { version = "0.4" }
100103
regex-syntax = { version = "0.8" }
@@ -132,8 +135,8 @@ url = { version = "2", features = ["serde"] }
132135
uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
133136
wasm-encoder = { version = "0.221", features = ["wasmparser"] }
134137
wasmparser = { version = "0.221" }
135-
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "parallel-compilation"] }
136-
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc"] }
138+
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
139+
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
137140
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
138141
winnow = { version = "0.7", features = ["simd"] }
139142

@@ -168,7 +171,9 @@ chrono = { version = "0.4", features = ["serde"] }
168171
clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
169172
clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
170173
concurrent-queue = { version = "2" }
174+
cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
171175
crc32fast = { version = "1" }
176+
crossbeam-epoch = { version = "0.9" }
172177
crossbeam-utils = { version = "0.8" }
173178
deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
174179
digest = { version = "0.10", features = ["mac", "oid", "std"] }
@@ -224,6 +229,7 @@ quote = { version = "1" }
224229
rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
225230
rand_chacha = { version = "0.3" }
226231
rand_core = { version = "0.6", default-features = false, features = ["std"] }
232+
regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
227233
regex = { version = "1" }
228234
regex-automata = { version = "0.4" }
229235
regex-syntax = { version = "0.8" }
@@ -267,8 +273,8 @@ url = { version = "2", features = ["serde"] }
267273
uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
268274
wasm-encoder = { version = "0.221", features = ["wasmparser"] }
269275
wasmparser = { version = "0.221" }
270-
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "parallel-compilation"] }
271-
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc"] }
276+
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
277+
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
272278
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
273279
winnow = { version = "0.7", features = ["simd"] }
274280

0 commit comments

Comments
 (0)