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

Adding a test that will test the correctness of snmalloc #569

Merged
merged 1 commit into from
Apr 1, 2024

Conversation

NirjharRoyiitk
Copy link
Contributor

@NirjharRoyiitk NirjharRoyiitk commented Mar 25, 2024

This is a test that tests the correctness of our new allocator snmalloc. This test should run continuosly and should not crash.

The pseudocode is taken from https://fortanix.atlassian.net/wiki/spaces/PLAT/pages/3155820642/Memory+Allocators

We should modify the configurable parameters https://github.com/fortanix/rust-sgx/blob/memory-correctness-test/examples/mem-correctness-test/src/main.rs#L43 and also modify the heap-size parameter in rust-sgx/examples/mem-correctness-test/Cargo.toml file then simply do the following

    cargo run --target x86_64-fortanix-unknown-sgx

arai-fortanix
arai-fortanix previously approved these changes Mar 25, 2024
Copy link
Contributor

@arai-fortanix arai-fortanix left a comment

Choose a reason for hiding this comment

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

I don't know how Rust vecs interact with the underlying heap allocator, but as a future improvement I think it probably makes sense to exercise resize() on existing vecs to grow and shrink them, in addition to allocating and freeing vecs.

It might also be nice to have command-line arguments or environment variables or some other mechanism for controlling the number of threads and number of iterations without having to recompile the code.

[package.metadata.fortanix-sgx]
# heap size (in bytes), the default heap size is 0x2000000.
heap-size=0x800000000
debug=false
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason we're setting debug=false here?

const TO_GB: usize = TO_MB * 1024;

const NUM_OPERATION_CHOICES: usize = 4;
const MAGIC_NUM_TO_CHECK: u32 = 0xdeadbeef;
Copy link
Contributor

Choose a reason for hiding this comment

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

I have a slight preference for not using 0xdeadbeef as a magic number. This is a popular choice for magic number, so if we're using 3rd party libraries, there's a slightly higher chance of this particular magic number being used by something else, compared to picking some different magic number. Also, 32 bits is pretty small if it's important that we don't encounter the magic number on accident. 64 bits would be better, if that's possible.

Both of these comments are very minor.

* can check for the same index multiple times. We could have checked
* for all the indices but that would be too time consuming
*/
if (buf[get_random_num(0, buf.len() - 1)] != MAGIC_NUM_TO_CHECK) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe as a future improvement, it may be a good idea to fill the buffer with pseudo-random data that can be easily checked, rather than by repeating the same bytes over and over. I've worked on memory allocators in the past that had bugs that manifested as data being copied to the wrong location in memory. These bugs were not caught by a test similar to this one that repeated the same magic number repeatedly, because all of the magic numbers were the same. When we changed the test to use pseudo-random (but calculatable) sequences, the test was able to catch the bug.

}

fn select_and_check_random_buffer_contents(array_of_vectors: &Vec<Vec<u32>>) {
/* This buffer selects a random number of buffers and then calls
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the comment say "This function selects a random number..." instead of "This buffer selects..."?

let num_active_vectors = array_of_vectors.len();
if (num_active_vectors > 0) {
let random_buffer_check_count =
get_random_num(1, 100) % MAX_BUFFER_CHECKS_PER_THREAD_ITERATION + 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to do this calculation this way instead of just calling get_random_num(1, MAX_BUFFER_CHECKS_PER_THREAD_ITERATION)?

let random_buffer_check_count =
get_random_num(1, 100) % MAX_BUFFER_CHECKS_PER_THREAD_ITERATION + 1;
for i in 1..=random_buffer_check_count {
let random_buffer_index_to_check = get_random_num(1, 100) % array_of_vectors.len();
Copy link
Contributor

Choose a reason for hiding this comment

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

Use get_random_num(0, array_of_vectors.len() - 1)?

let random_index_to_delete = get_random_num(1, 100) % array_of_vectors.len();
let len = &array_of_vectors[random_index_to_delete].len() * mem::size_of::<i32>();
drop(&array_of_vectors[random_index_to_delete]);
array_of_vectors.remove(random_index_to_delete);
Copy link
Contributor

Choose a reason for hiding this comment

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

Does removing the vec from array_of_vectors not cause the vec to be dropped?

const MAX_BUFF_NUM_PER_THREAD: usize = (32);
const MAX_BUFFER_CHECKS_PER_THREAD_ITERATION: usize = 8;
const MAX_INDEX_CHECKS_PER_BUFFER: usize = 1 << 16;
const MAX_ALLOWED_FREE_HEAP_PERCENTAGE: f64 = 80.0;
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: Maybe this should be named MAX_ALLOWED_USED_HEAP_PERCENTAGE?

const TO_MB: usize = TO_KB * 1024;
const TO_GB: usize = TO_MB * 1024;

const NUM_OPERATION_CHOICES: usize = 4;

Choose a reason for hiding this comment

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

why don't you create an array of tester functions: fn traverse_and_check_buffer(buf: &Vec)
and in the worker thread we just go by the number of elements in the array.
this way we can:

  • expand the test variety by adding more operations
  • change the test type weight/ratio by adding more functions of the same type
  • remove test types, for debug purposes or when they become obsolete, or for whatever other reason
  • other fun stuff

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this idea (as a future enhancement), but I would add that the usual way to have different weights for different functions is not to duplicate their entries in the table but rather to have an explicit weight associated with each table entry, and then randomly choose an activity taking the weights into account.

@NirjharRoyiitk NirjharRoyiitk force-pushed the memory-correctness-test branch 2 times, most recently from daa696a to 1588dc0 Compare March 26, 2024 14:52
/* Set of configurable parameters. These will adjusted as necessary while
* recording the performance numbers
*/
const NUM_CPUS: usize = 64;

Choose a reason for hiding this comment

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

Would be nice to have an explanation or a one line comment about each of these parameters.

@NirjharRoyiitk NirjharRoyiitk force-pushed the memory-correctness-test branch from 1588dc0 to 16b1c28 Compare March 26, 2024 15:09
drop(started);
}

fn traverse_and_check_buffer(buf: &Vec<usize>, MAGIC_NUM_TO_CHECK: usize) {

Choose a reason for hiding this comment

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

Change MAGIC_NUM_TO_CHECK to snake case as it is a dynamic value

Choose a reason for hiding this comment

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

You can compile this program and address the warnings generated by the rust compiler, I see a lot of unused variables or improvements that can be easily addressed:

warning: unused import: `num_cpus`
  --> src/main.rs:20:5
   |
20 | use num_cpus;
   |     ^^^^^^^^
   |
   = note: `#[warn(unused_imports)]` on by default

warning: unused import: `std::time::Instant`
  --> src/main.rs:26:5
   |
26 | use std::time::Instant;
   |     ^^^^^^^^^^^^^^^^^^

warning: unnecessary parentheses around assigned value
  --> src/main.rs:45:47
   |
45 | const PER_THREAD_PER_BUFFER_MAX_SIZE: usize = (4 * TO_KB);
   |                                               ^         ^
   |
   = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
   |
45 - const PER_THREAD_PER_BUFFER_MAX_SIZE: usize = (4 * TO_KB);
45 + const PER_THREAD_PER_BUFFER_MAX_SIZE: usize = 4 * TO_KB;
   |

Copy link
Contributor

Choose a reason for hiding this comment

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

You can also turn on a setting that makes warning fail the build.

@NirjharRoyiitk NirjharRoyiitk force-pushed the memory-correctness-test branch from 16b1c28 to bf4419d Compare March 26, 2024 15:13
@arai-fortanix arai-fortanix self-requested a review March 26, 2024 15:26
* then n threads, then n/2 and so on.
*/
fn start_tests() {
let MAGIC_NUM_TO_CHECK = get_random_num(1, usize::MAX);
Copy link
Contributor

Choose a reason for hiding this comment

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

My intention with better randomization of the fill data was that every element of every vec should get a distinct value. I think that's fine as a future enhancement.

@NirjharRoyiitk NirjharRoyiitk force-pushed the memory-correctness-test branch from bf4419d to 62b040c Compare March 26, 2024 19:41
@arai-fortanix arai-fortanix self-requested a review March 26, 2024 19:44
arai-fortanix
arai-fortanix previously approved these changes Mar 26, 2024

fn get_free_heap_percentage() -> f64 {
(get_free_heap_size_in_bytes() as f64 / heap_size() as f64) * 100.0
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This and the functions above could be logically grouped in Heap struct. That'd also simplify their names. You could do the same for allocating, freeing and checking allocated chunks.

drop(started);
}

fn wakeup_all_child_threads(pair_clone: Arc<(Mutex<bool>, Condvar)>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use a Barrier here

}

fn traverse_and_check_buffer(buf: &Vec<usize>, magic_num_to_check: usize) {
let num_indices_checks = get_random_num(1, MAX_INDEX_CHECKS_PER_BUFFER);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you have the same issue here as in #566: You allocate a Vec assuming each element is a byte, which is not the case

* can check for the same index multiple times. We could have checked
* for all the indices but that would be too time consuming
*/
if buf[get_random_num(0, buf.len() - 1)] != magic_num_to_check {
Copy link
Contributor

Choose a reason for hiding this comment

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

I liked Dan's comment on magic numbers. You could even calculate a checksum instead

}
}

fn get_num_processors() -> usize {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is confusing as get_num_processors doesn't return the number of processors on the machine

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As an optimization I will fill up this function with so that it returns number of processors. Currently it returns a hardcoded value

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes but if it's hardcoded, there's no need to have this function

* than MIN_ALLOWED_FREE_HEAP_PERCENTAGE
*/

let random_size = get_random_num(mem::size_of::<usize>() * 2, PER_THREAD_PER_BUFFER_MAX_SIZE)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can the minimum be 1 byte?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially tried with 1 byte but since the vector is of of u64, I kept the minimum size to be 16 bytes

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes that type probably comes from how you compare it with a magic number. You could use a checksum instead

let random_size = get_random_num(mem::size_of::<usize>() * 2, PER_THREAD_PER_BUFFER_MAX_SIZE)
/ mem::size_of::<usize>();
let mut tmp: Vec<usize> = Vec::with_capacity(random_size);
tmp.resize(random_size, magic_num_to_check);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you interact with the memory allocator directly? Vec may internally do some optimizations. For example, allocating more than requested. That'd mess up the stress test for the allocator

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't tried interacting with the allocator directly. Do you want me to try this in this PR? @raoulstrackx

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, please

extern "C" {
static HEAP_BASE: u64;
static HEAP_SIZE: usize;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Pretty much all this code could also be executed on almost all targets the Rust compiler supports, except some statements/functions like these. You could restrict these to only the SGX target with:

#[cfg(target_env = "sgx")]
extern "C" {
    static HEAP_BASE: u64;
    static HEAP_SIZE: usize;
}

That way you can develop and test under Linux, and if that works, try it within an enclave.

@NirjharRoyiitk NirjharRoyiitk force-pushed the memory-correctness-test branch 2 times, most recently from 7a8e2d4 to 7cad3fc Compare April 1, 2024 10:12
@NirjharRoyiitk NirjharRoyiitk force-pushed the memory-correctness-test branch from 7cad3fc to a28a97a Compare April 1, 2024 12:25
@arai-fortanix arai-fortanix self-requested a review April 1, 2024 16:27
@NirjharRoyiitk NirjharRoyiitk added this pull request to the merge queue Apr 1, 2024
Merged via the queue into master with commit dda4629 Apr 1, 2024
1 check passed
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.

5 participants