Skip to content
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

Merged
merged 15 commits into from
Mar 10, 2021
Merged

Concurrent preloader for contracts #3985

merged 15 commits into from
Mar 10, 2021

Conversation

olonho
Copy link
Contributor

@olonho olonho commented Feb 19, 2021

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.

time cargo test --release  --color=always --test test_vm_runner test_vm_run_sequential --no-fail-fast --manifest-path runtime/near-vm-runner/Cargo.toml -- --format=json --exact -Z unstable-options --show-output
time cargo test --release  --color=always --test test_vm_runner test_vm_run_preloaded --no-fail-fast --manifest-path runtime/near-vm-runner/Cargo.toml -- --format=json --exact -Z unstable-options --show-output

Sequential execution (0ms cache delay):

real	0m1.152s
user	0m0.934s
sys	0m0.241s

Preloaded execution (0ms cache delay):

real	0m0.810s
user	0m1.202s
sys	0m0.367

Sequential execution (5ms cache delay):

real	0m2.726s
user	0m1.277s
sys	0m0.305s

Preloaded execution (5ms cache delay):

real	0m1.021s
user	0m1.133s
sys	0m0.309s

So with realistic cache delays we get almost 3x speedup.

@olonho olonho requested a review from ailisp as a code owner February 19, 2021 19:00
@olonho olonho changed the title WIP of concurrent launcher Concurrent preloader for contracts Mar 6, 2021
@olonho olonho force-pushed the preinit_vm branch 2 times, most recently from b7b181a to 0b90ed1 Compare March 9, 2021 09:59
self.pool.execute(move || {
prepare_in_thread(copy_request, vm_kind, tx);
});
result.push(ContractCallPrepareResult { handle: Some(index), error: None });
Copy link
Member

@ailisp ailisp Mar 9, 2021

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?

Copy link
Contributor Author

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())),
Copy link
Member

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]?

Copy link
Contributor Author

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.

Copy link
Member

@ailisp ailisp left a 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

runtime/near-vm-runner/src/preload.rs Outdated Show resolved Hide resolved
runtime/near-vm-runner/src/preload.rs Outdated Show resolved Hide resolved
runtime/near-vm-runner/src/preload.rs Outdated Show resolved Hide resolved
Comment on lines 40 to 43
pub struct ContractCallPrepareResult {
pub handle: Option<usize>,
pub error: Option<VMError>,
}
Copy link
Contributor

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

Suggested change
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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

runtime/near-vm-runner/src/preload.rs Outdated Show resolved Hide resolved
runtime/near-vm-runner/src/preload.rs Outdated Show resolved Hide resolved
Comment on lines +273 to +280
if method_name.is_empty() {
return (
None,
Some(VMError::FunctionCallError(FunctionCallError::MethodResolveError(
MethodResolveError::MethodEmptyName,
))),
);
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

olonho and others added 8 commits March 10, 2021 13:28
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" }
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, done

@olonho olonho merged commit b05da04 into master Mar 10, 2021
@olonho olonho deleted the preinit_vm branch March 10, 2021 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants