-
Notifications
You must be signed in to change notification settings - Fork 627
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
Concurrent preloader for contracts #3985
Conversation
b7b181a
to
0b90ed1
Compare
self.pool.execute(move || { | ||
prepare_in_thread(copy_request, vm_kind, tx); | ||
}); | ||
result.push(ContractCallPrepareResult { handle: Some(index), error: None }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the reason of handle need to wrap in Some
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle may be invalid, and I want that explicit, not by magical value.
profile: ProfileData, | ||
) -> (Option<VMOutcome>, Option<VMError>) { | ||
match &prepared.error { | ||
Some(error) => return (None, Some(error.clone())), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this would never be error because it's constructed by:
result.push(ContractCallPrepareResult { handle: Some(index), error: None });
Prepare error would happen here:
tx.send(VMCallData { result }).unwrap();
and are saved in self.prepared (Vec<CallInner>
). so i think to process error, this should refer to self.prepared[prepared.handle]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, made it somewhat simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I think the benefit is significant. a few nit comment
pub struct ContractCallPrepareResult { | ||
pub handle: Option<usize>, | ||
pub error: Option<VMError>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we can simplify this to just
pub struct ContractCallPrepareResult { | |
pub handle: Option<usize>, | |
pub error: Option<VMError>, | |
} | |
pub struct ContractCallPrepareResult { | |
rx: Receiver<VMCallData>, | |
} |
that is, we don't need CallInner
and the prepared
vector, PrepareResult
itself could carry all the data necessary for resolution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, don't want to expose implementation details (such as VM implementation dependencies) in public API of preloader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But the rx
can be private, no?
if method_name.is_empty() { | ||
return ( | ||
None, | ||
Some(VMError::FunctionCallError(FunctionCallError::MethodResolveError( | ||
MethodResolveError::MethodEmptyName, | ||
))), | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's interesting! In principle, we could move this error to prepare stage. But that would be a bad idea, because we need to preserve semantics of sequential execution. More generally, prepare call should never fail, all errors should be moved to the moment we actually run the call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also current semantics of preloader is that we preload the contract, and select method to execute later on.
To minimize deserialization delays we introduce mechanism of pipelined constructing WASM modules before execution. We concurrent deserialize multiple contracts, and use those deserialized contracts for execution in single thread. So no concurrency issues should happen, and execution is fully deterministic.
Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
Cargo.toml
Outdated
@@ -63,6 +63,10 @@ near-chain = { path = "./chain/chain" } | |||
|
|||
node-runtime = { path = "./runtime/runtime" } | |||
|
|||
near-vm-logic = { path = "./runtime/near-vm-logic" } | |||
near-vm-errors = { path = "./runtime/near-vm-errors" } | |||
near-vm-runner = { path = "./runtime/near-vm-runner" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, wait, why we need these here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without it doesn’t compile, IIRC. Does it for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right, thats' because we add a test to the root package. Naively, I'd expect the test to be within near-vm-runner
crate, like this:
diff --git a/Cargo.lock b/Cargo.lock
index 032d7eb9..60f5dd02 100644
Binary files a/Cargo.lock and b/Cargo.lock differ
diff --git a/Cargo.toml b/Cargo.toml
index b19e030e..1dd38eb4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,10 +63,6 @@ near-chain = { path = "./chain/chain" }
node-runtime = { path = "./runtime/runtime" }
-near-vm-logic = { path = "./runtime/near-vm-logic" }
-near-vm-errors = { path = "./runtime/near-vm-errors" }
-near-vm-runner = { path = "./runtime/near-vm-runner" }
-
near-jsonrpc = { path = "./chain/jsonrpc" }
near-network = { path = "./chain/network" }
diff --git a/runtime/near-vm-runner/src/tests.rs b/runtime/near-vm-runner/src/tests.rs
index 34d6636a..7cbaf3cf 100644
--- a/runtime/near-vm-runner/src/tests.rs
+++ b/runtime/near-vm-runner/src/tests.rs
@@ -1,5 +1,6 @@
mod error_cases;
mod invalid_contracts;
+mod contract_preload;
mod rs_contract;
mod ts_contract;
diff --git a/tests/test_vm_runner.rs b/runtime/near-vm-runner/src/tests/contract_preload.rs
similarity index 96%
rename from tests/test_vm_runner.rs
rename to runtime/near-vm-runner/src/tests/contract_preload.rs
index 1cd42463..3ece2215 100644
--- a/tests/test_vm_runner.rs
+++ b/runtime/near-vm-runner/src/tests/contract_preload.rs
@@ -1,7 +1,6 @@
use near_primitives::hash::hash;
use near_primitives::runtime::fees::RuntimeFeesConfig;
use near_vm_logic::{ProtocolVersion, VMConfig, VMContext, VMKind, VMOutcome};
-use near_vm_runner::{run_vm, ContractCallPrepareRequest, ContractCaller, VMError};
use near_primitives::borsh::BorshSerialize;
use near_primitives::types::CompiledContractCache;
@@ -13,10 +12,13 @@ use std::sync::{Arc, Mutex};
use std::thread::sleep;
use std::time::Duration;
+use crate::{run_vm, ContractCallPrepareRequest, ContractCaller, VMError};
+
+
const TEST_CONTRACT_1: &'static [u8] =
- include_bytes!("../runtime/near-vm-runner/tests/res/test_contract_rs.wasm");
+ include_bytes!("../../tests/res/test_contract_rs.wasm");
const TEST_CONTRACT_2: &'static [u8] =
- include_bytes!("../runtime/near-vm-runner/tests/res/test_contract_ts.wasm");
+ include_bytes!("../../tests/res/test_contract_ts.wasm");
fn default_vm_context() -> VMContext {
return VMContext {
But I don't actually know the rule we use to decide if we want to put a test into the top-level tests directory, or into a specific crate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually that's exactly how it used to be, and you suggested to move it here in earlier comments. I'm kind of OK with either approach, but keep moving it forward and backward doesn't make much sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ouch, I see now that my suggestion was ambiguous, sorry about that.
Originally, the test was in (from the root of repository)
/runtime/near-vm-runner/tests/test_vm_runner.rs
- it should be moved to
/runtime/near-vm-runner/src/tests/test_vm_runner.rs
(hence my suggestion to move it to src/tests, and hence the diff above) - at the moment, it is at
/tests/test_vm_runner.rs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, done
To minimize deserialization delays we introduce mechanism of pipelined constructing
WASM modules before execution.
On simulated tests (with disabled in-memory instance cache) we get the following results.
Sequential execution (0ms cache delay):
Preloaded execution (0ms cache delay):
Sequential execution (5ms cache delay):
Preloaded execution (5ms cache delay):
So with realistic cache delays we get almost 3x speedup.