-
Notifications
You must be signed in to change notification settings - Fork 68
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
jco componentize out of memory #568
Comments
Quick update: after a bit of hacking around, I was able to get it to build a component. The way I got it to build was by deferring the initialization. The few megabytes of structured data are still there, but I lazily do the initialization that processes them (ex: turning a dictionary with string keys into regex objects). So it seems the initial limit is something to do with pre initializing the WASM environment. Is there a way to raise the limits? It would be perfectly acceptable for our use case to have a larger pre initialized memory :) (The resulting wasm is pretty large btw around 45MB but that's fine for my use case.) UPDATE 2: I was able to get it in a Rust host app, but ended up hitting a "wasm UPDATE 3: I eventually got it working under some circumstances, but with the following caveats. My original approach to the port was pretty dumb and each "file" was just mapped into a JS object that held strings. Running this logic as part of the main init process caused an OOM. Reworking SOME of the processing logic (basically splitting the files into lines and then splitting each line on some delimiter and flattening into a single array) made the difference between being able to build the module or not. However, the "lazy init" approach caused some issues; it would hit the unreachable instruction error, whereas I was able to eliminate this by doing the heavy loading into a "global" at the start of the JS entry point. This is probably a better idea anyways :) I just had to do a lot more work to avoid hitting either an OOM at The other annoying part is that this baloons the final component size to 94MB 😅 For reference, the bundled JavaScript is around 10MB (most of this being data files). In addition to what feels like some sort of memory limit during componentization, the other unsolved mystery is why AOT doesn't work. In the final working configuration, I get the following error with the
This sounds like it might be an issue with weval, not jco, but recording the experience in case it's helpful. I should be able to post this all up in a repo with an MRE sometime this week. |
I apologize that this has turned into a bit of an "I'm not sure what's going on" issue; please feel free to split into sub-issues as you can identify them... Here are some more confusing points which may or may not be bugs in Deterministic failure after multiple executionsAfter some number of executions in a loop using The weird (but I guess it should be comforting) part is that it's completely deterministic for a given wasm component build. Changing seemingly insignificant portions of the code that should not affect functionality can affect the number of iterations required before hitting an unreachable instruction. I suspected there was a I've also tried setting custom As such, it seems like it's some sort of bug. AOT compilationI was eventually able to get it compiled with Unfortunately, the AOT compiled version doesn't work. It hits an Example to make things concreteRepo that builds the component: https://github.com/stadiamaps/pelias-parser-component `States` struct (basically from wasmtime examples)struct States {
table: ResourceTable,
ctx: WasiCtx,
limits: StoreLimits,
}
impl States {
pub fn new() -> Self {
let table = ResourceTable::new();
let ctx = WasiCtxBuilder::new().build();
let limits = StoreLimitsBuilder::new()
.instances(1_000_000)
.tables(1_000_000)
.memories(1_000_000)
.trap_on_grow_failure(true)
.build();
Self {
table,
ctx,
limits
}
}
}
impl WasiView for States {
fn table(&mut self) -> &mut ResourceTable {
&mut self.table
}
fn ctx(&mut self) -> &mut WasiCtx {
&mut self.ctx
}
} wasmtime component bindgen invocationbindgen!({
path: "parser.wit",
world: "address-parser",
async: false,
additional_derives: [
serde::Deserialize,
serde::Serialize,
Clone,
Hash,
],
}); Wrapper to make invocation easierpub struct Parser {
store: Store<States>,
// This is an attempt at speeding up failure recovery.
// Creating a new instance every time adds 7-10ms, which is less than ideal.
instance_pre: component::AddressParserPre<States>,
instance: AddressParser,
}
impl Parser {
pub fn init_with_engine(engine: &Engine) -> wasmtime::Result<Self> {
let start = Instant::now();
let component = Component::from_file(engine, "/path/to/pelias-parser-component/dist/parser.wasm")?;
eprintln!("Component init after {:?}", start.elapsed());
// Construct store for storing running states of the component
let wasi_view = States::new();
let mut store = Store::new(engine, wasi_view);
store.limiter(|state| &mut state.limits); // Custom limits config; probably not necessary
let linker = Linker::new(engine);
let pre = linker.instantiate_pre(&component)?;
let instance_pre = component::AddressParserPre::new(pre)?;
let instance = component::AddressParser::instantiate(&mut store, &component, &linker)?;
eprintln!("Instance init after {:?}", start.elapsed());
Ok(Self {
store,
instance_pre,
instance,
})
}
// The main entry point. Inline probably not necessary; doesn't seem to make a difference in benchmarks.
#[inline]
pub fn parse(&mut self, input: &str) -> anyhow::Result<ParsedComponents> {
// let instance = component::AddressParser::instantiate(&mut self.store, &self.component, &self.linker)?;
// let instance = self.instance_pre.instantiate(&mut self.store)?;
match self.instance.call_parse(&mut self.store, input) {
Ok(comps) => Ok(comps),
Err(e) => {
// Failure recovery path to recreate the component.
// Removing this will let you see which iteration the bench fails on.
let start = Instant::now();
self.instance = self.instance_pre.instantiate(&mut self.store)?;
eprintln!("Re-instantiated after {:?} due to {e:?}", start.elapsed());
self.instance.call_parse(&mut self.store, input)
}
}
}
} Benchmark codeuse std::time::Instant;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use wasmtime::{Config, Engine, OptLevel};
use pelias_parser::Parser;
pub fn criterion_benchmark(c: &mut Criterion) {
let input = "30 w 26 st nyc 10010";
let mut config = Config::new();
config.cranelift_opt_level(OptLevel::Speed);
let engine = Engine::new(&config).expect("Unable to create engine");
let mut parser = Parser::init_with_engine(&engine).unwrap();
let start = Instant::now();
let mut iter = 0;
let mut failures = 0;
c.bench_with_input(BenchmarkId::new("parse", input), &input.to_string(), |b, i| b.iter(|| {
iter += 1;
let res = parser.parse(i);
if res.is_err() {
if failures == 0 {
eprintln!("#{iter} @ {:?} {res:?}", start.elapsed());
}
failures += 1;
}
}));
if failures > 0 {
eprintln!("{failures} / {iter} iterations failed");
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches); |
Hey all, thanks for the cool project! I've been following various talks in the space for the last year and am really excited to be trying out some stuff!
Background
I'm working in a Rust codebase that's replacing a bunch of JavaScript. To accelerate the process, I'm looking at componentizing some of the JS modules.
The first project I picked is a bit of a doozy since, while the business logic is pretty straightforward and doesn't use too much craziness, the core is wholly dependent on some files on disk which consist of rules that drive things. This turns out to be fairly large; several megabytes worth of data uncompressed.
Here's my WIT file; it's pretty basic. Just take some input and spit out some output (JSON, but I'll deal with better types later).
Initial approach
I could theoretically replace most portions of the library that currently rely on node fs and path APIs with WASI equivalents and hard-coded values respectively, but it seemed like lot less trouble to just bake these assets into the package. So, a few swings and a lot of
jq
later, I've transformed all of the file code intorequire('./data.js')
essentially. It works!!I've also figured out what seems to be a working
rollup
config and worked that into my tooling. There are now no external imports, andjco componentize
gets past the first steps that otherwise failed when I had a dependency onfs
orpath
.The issue
When I run
jco componentize
, I get the following error:The full command, in case it's useful, is
jco componentize --wit wit --world-name parser -o dist/parser.wasm bundle/parser.bundled.js
.I also tried it with the
--aot
flag and, after like 10 seconds, I got a similar error message.I have a hunch that this is something to do with the size of the code, but I figured I'd ask the experts before spinning my wheels too much more, as I may have just missed something dumb.
Thanks in advance!
The text was updated successfully, but these errors were encountered: