Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Decommit instance memory after a runtime call on Linux #8998

Merged
12 commits merged into from
Jun 14, 2021
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ sc-tracing = { version = "3.0.0", path = "../tracing" }
tracing = "0.1.25"
tracing-subscriber = "0.2.18"
paste = "1.0"
regex = "1"

[features]
default = [ "std" ]
Expand Down
9 changes: 9 additions & 0 deletions client/executor/common/src/wasm_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,13 @@ pub trait WasmInstance: Send {
///
/// This method is only suitable for getting immutable globals.
fn get_global_const(&self, name: &str) -> Result<Option<Value>, Error>;

/// **Testing Only**. This function returns the base address of the linear memory.
///
/// This is meant to be the starting address of the memory mapped area for the linear memory.
///
/// This function is intended only for a specific test that measures physical memory consumption.
fn linear_memory_base_ptr(&self) -> Option<*const u8> {
None
}
}
30 changes: 30 additions & 0 deletions client/executor/runtime-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,36 @@ sp_core::wasm_export_functions! {

fn test_empty_return() {}

fn test_dirty_plenty_memory(heap_base: u32, heap_pages: u32) {
// This piece of code will dirty multiple pages of memory. The number of pages is given by
// the `heap_pages`. It's unit is a wasm page (64KiB). The first page to be cleared
// is a wasm page that that follows the one that holds the `heap_base` address.
//
// This function dirties the **host** pages. I.e. we dirty 4KiB at a time and it will take
// 16 writes to process a single wasm page.

let mut heap_ptr = heap_base as usize;

// Find the next wasm page boundary.
let heap_ptr = round_up_to(heap_ptr, 65536);

// Make it an actual pointer
let heap_ptr = heap_ptr as *mut u8;

// Traverse the host pages and make each one dirty
let host_pages = heap_pages as usize * 16;
for i in 0..host_pages {
unsafe {
// technically this is an UB, but there is no way Rust can find this out.
heap_ptr.add(i * 4096).write(0);
}
}
pepyakin marked this conversation as resolved.
Show resolved Hide resolved

fn round_up_to(n: usize, divisor: usize) -> usize {
(n + divisor - 1) / divisor
}
}

fn test_exhaust_heap() -> Vec<u8> { Vec::with_capacity(16777216) }

fn test_panic() { panic!("test panic") }
Expand Down
68 changes: 68 additions & 0 deletions client/executor/src/integration_tests/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// This file is part of Substrate.

// Copyright (C) 2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Tests that are only relevant for Linux.

// Constrain this only to wasmtime for the time being. Without this rustc will complain on unused
// imports and items. The alternative is to plop `cfg(feature = wasmtime)` everywhere which seems
// borthersome.
#![cfg(feature = "wasmtime")]

use crate::WasmExecutionMethod;
use super::mk_test_runtime;
use codec::Encode as _;

mod smaps;

use self::smaps::Smaps;

#[test]
fn memory_consumption_compiled() {
pepyakin marked this conversation as resolved.
Show resolved Hide resolved
let runtime = mk_test_runtime(WasmExecutionMethod::Compiled, 1024);

let instance = runtime.new_instance().unwrap();
let heap_base = instance
.get_global_const("__heap_base")
.expect("`__heap_base` is valid")
.expect("`__heap_base` exists")
.as_i32()
.expect("`__heap_base` is an `i32`");

let smaps = Smaps::new();

instance
.call_export(
"test_dirty_plenty_memory",
&(heap_base as u32, 1u32).encode(),
)
.unwrap();

// Just assume that the linear memory is backed by a single memory mapping.
let base_addr = instance.linear_memory_base_ptr().unwrap() as usize;
let rss_pre = smaps.get_rss(base_addr).unwrap();

instance
.call_export(
"test_dirty_plenty_memory",
&(heap_base as u32, 1024u32).encode(),
)
.unwrap();
let rss_post = smaps.get_rss(base_addr).unwrap();
pepyakin marked this conversation as resolved.
Show resolved Hide resolved

assert_eq!(rss_post.saturating_sub(rss_pre), 0);
pepyakin marked this conversation as resolved.
Show resolved Hide resolved
}
82 changes: 82 additions & 0 deletions client/executor/src/integration_tests/linux/smaps.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// This file is part of Substrate.

// Copyright (C) 2017-2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! A tool for extracting information about the memory consumption of the current process from
//! the procfs.

use std::ops::Range;
use std::collections::BTreeMap;

/// An interface to the /proc/self/smaps
///
/// See docs about [procfs on kernel.org][procfs]
///
/// [procfs]: https://www.kernel.org/doc/html/latest/filesystems/proc.html
pub struct Smaps(Vec<(Range<usize>, BTreeMap<String, usize>)>);

impl Smaps {
pub fn new() -> Self {
let regex_start = regex::RegexBuilder::new("^([0-9a-f]+)-([0-9a-f]+)")
.multi_line(true)
.build()
.unwrap();
let regex_kv = regex::RegexBuilder::new(r#"^([^:]+):\s*(\d+) kB"#)
.multi_line(true)
.build()
.unwrap();
let smaps = std::fs::read_to_string("/proc/self/smaps").unwrap();
let boundaries: Vec<_> = regex_start
.find_iter(&smaps)
.map(|matched| matched.start())
.chain(std::iter::once(smaps.len()))
.collect();

let mut output = Vec::new();
for window in boundaries.windows(2) {
let chunk = &smaps[window[0]..window[1]];
let caps = regex_start.captures(chunk).unwrap();
let start = usize::from_str_radix(caps.get(1).unwrap().as_str(), 16).unwrap();
let end = usize::from_str_radix(caps.get(2).unwrap().as_str(), 16).unwrap();

let values = regex_kv
.captures_iter(chunk)
.map(|cap| {
let key = cap.get(1).unwrap().as_str().to_owned();
let value = cap.get(2).unwrap().as_str().parse().unwrap();
(key, value)
})
.collect();

output.push((start..end, values));
}

Self(output)
}

fn get_map(&self, addr: usize) -> &BTreeMap<String, usize> {
&self.0
.iter()
.find(|(range, _)| addr >= range.start && addr < range.end)
.unwrap()
.1
}

pub fn get_rss(&self, addr: usize) -> Option<usize> {
self.get_map(addr).get("Rss").cloned()
}
Comment on lines +71 to +81
Copy link
Member

Choose a reason for hiding this comment

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

How sure are you that the whole instance memory is kept in one mapping? I think they can also be split up in multiple consecutive mappings (at least on macOS that is the case).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is Linux only code so we don't care about macOS here. Yes, potentially they can be split up on several mappings. The opposite is also possible AFAIU, when several instances are packed into a single VMA. Note though that the solution with madvise should work in these cases.

We generally cannot be 100% sure what wasmtime does under the hood, so this is the best effort. But hey it's not the end of the world if this test fails. I see no reason it to be not a single mapping though, at least on Linux. It is simpler to implement, most efficient, etc

}
12 changes: 12 additions & 0 deletions client/executor/src/integration_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

#[cfg(target_os = "linux")]
mod linux;
mod sandbox;

use std::sync::Arc;
Expand Down Expand Up @@ -64,6 +67,15 @@ macro_rules! test_wasm_execution {
}
}
};

(compiled_only $method_name:ident) => {
paste::item! {
#[test]
fn [<$method_name _compiled>]() {
$method_name(WasmExecutionMethod::Compiled);
}
}
};
pepyakin marked this conversation as resolved.
Show resolved Hide resolved
}

fn call_in_wasm<E: Externalities>(
Expand Down
2 changes: 2 additions & 0 deletions client/executor/wasmtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ readme = "README.md"
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
libc = "0.2.90"
cfg-if = "1.0"
log = "0.4.8"
scoped-tls = "1.0"
parity-wasm = "0.42.0"
Expand Down
37 changes: 37 additions & 0 deletions client/executor/wasmtime/src/instance_wrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,43 @@ impl InstanceWrapper {
slice::from_raw_parts_mut(ptr, len)
}
}

/// Returns the pointer to the first byte of the linear memory for this instance.
pub fn base_ptr(&self) -> *const u8 {
self.memory.data_ptr()
}

/// Removes physical backing from the allocated linear memory. This leads to returning the memory
/// back to the system. While the memory is zeroed this is considered as a side-effect and is not
/// relied upon. Thus this function acts as a hint.
pub fn decommit(&self) {
if self.memory.data_size() == 0 {
return;
}

cfg_if::cfg_if! {
Copy link
Member

Choose a reason for hiding this comment

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

We don't need cfg_if 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.

You mean that madvise is POSIX-compatible? Sure, but it's complicated. In Linux this is not merely an advice it has a very certain and widely relied semantics of eagerly freeing and zeroing the pages. macOS is, on the other hand, is very lazy about those. madvise(WONTNEED) won't have the same effect there.

There are different options there to make it work on macOS, but those are out of scope for this PR. When the time comes though it will be clear where to integrate them : )

Copy link
Member

Choose a reason for hiding this comment

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

I mean we don't need the cfg_if.

Could be just cfg(unix)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Assuming you meant cfg(target_os = "linux"))

Yeah, true, not necessary. I figured I use it because:

  • it's not a new dependency. We already use it and it is a very common dep that it occurs in our dep tree many times
  • as I mentioned it's likely we want other branches for macOS and/or cfg(unix)

It's not something I am married to, so can change to a plain cfg as well. Just let me know

Copy link
Member

Choose a reason for hiding this comment

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

Yeah fine

if #[cfg(target_os = "linux")] {
use std::sync::Once;

unsafe {
let ptr = self.memory.data_ptr();
let len = self.memory.data_size();

// Linux handles MADV_DONTNEED reliably. The result is that the given area
// is unmapped and will be zeroed on the next pagefault.
if libc::madvise(ptr as _, len, libc::MADV_DONTNEED) != 0 {
static LOGGED: Once = Once::new();
LOGGED.call_once(|| {
log::warn!(
"madvise(MADV_DONTNEED) failed: {}",
std::io::Error::last_os_error(),
);
});
}
}
}
}
}
}

impl runtime_blob::InstanceGlobals for InstanceWrapper {
Expand Down
21 changes: 20 additions & 1 deletion client/executor/wasmtime/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,13 @@ impl WasmInstance for WasmtimeInstance {
globals_snapshot.apply(&**instance_wrapper);
let allocator = FreeingBumpHeapAllocator::new(*heap_base);

perform_call(data, Rc::clone(&instance_wrapper), entrypoint, allocator)
let result = perform_call(data, Rc::clone(&instance_wrapper), entrypoint, allocator);

// Signal to the OS that we are done with the linear memory and that it can be
// reclaimed.
instance_wrapper.decommit();

result
}
Strategy::RecreateInstance(instance_creator) => {
let instance_wrapper = instance_creator.instantiate()?;
Expand All @@ -173,6 +179,19 @@ impl WasmInstance for WasmtimeInstance {
}
}
}

fn linear_memory_base_ptr(&self) -> Option<*const u8> {
match &self.strategy {
Strategy::RecreateInstance(_) => {
// We do not keep the wasm instance around, therefore there is no linear memory
// associated with it.
None
}
Strategy::FastInstanceReuse {
instance_wrapper, ..
} => Some(instance_wrapper.base_ptr()),
}
}
}

/// Prepare a directory structure and a config file to enable wasmtime caching.
Expand Down