Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Fix memory leak in instantiation_cache #2701

Closed
wants to merge 0 commits into from
Closed

Fix memory leak in instantiation_cache #2701

wants to merge 0 commits into from

Conversation

learnforpractice
Copy link
Contributor

No description provided.

@learnforpractice
Copy link
Contributor Author

#2694

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 2, 2018

In order to break the module cache check, eosio.token need a small hack, place the following code in eosio.token.cpp, testing code will replace "hello,world" with other characters in order to workaround the module cache check.

void sayHello() {
   prints("hello,world");
}

Run the following testing code under build/contracts

import os
import shlex
import subprocess

def toname(i):
    return chr(ord('A')+int(i%10)) + chr(ord('A')+int((i/10)%10)) + chr(ord('A')+int((i/100)%10))

cleos = './../programs/cleos/cleos'

create_account_cmd = '{0} create account eosio eosio.token EOS7ijWCBmoXBi3CgtK7DJxentZZeTkeUnaSDvyro9dq7Sd1C3dC4 EOS7ijWCBmoXBi3CgtK7DJxentZZeTkeUnaSDvyro9dq7Sd1C3dC4'.format(cleos)
create_account_cmd = shlex.split(create_account_cmd)
ret = subprocess.call(create_account_cmd)
'''
if ret != 0:
    raise Exception('Creating account failed')
'''

wast = 'eosio.token/eosio.token.wast'
if not os.path.exists(wast+'2'):
    with open(wast, 'rb') as f:
        with open(wast+'2', 'wb') as f2:
            data = f.read()
            f2.write(data)

for i in range(1, 1000):
    key_words = b"hello,world"
    with open(wast+'2', 'rb') as f:
        data = f.read()
        #data.find(key_words)
        replace_str = b"a%d"%(i,)
        replace_str.zfill(len(key_words))
        #replace key works with custom words to break the effect of code cache mechanism
        print(data.find(key_words))
        data = data.replace(key_words, replace_str)
        with open(wast, 'wb') as f:
            f.write(data)

    ret = subprocess.call(['./../programs/cleos/cleos', 'set', 'contract', 'eosio.token', 'eosio.token',  '-p', 'eosio.token'])
    if ret != 0:
        raise Exception('Setting contract failed')

    ret = subprocess.call(['./../programs/cleos/cleos', 'push', 'action', 'eosio.token', 'create','[ "eosio", "%d.0000 %s", 0, 0, 0]'%(i,toname(i)), '-p', 'eosio.token'])

Expecting to see no dramatically memory increase during the running of testing code.

@spoonincode
Copy link
Contributor

Keeping all the versions of code was an explicit choice at the time. Calling @wanderingbort what do you think?

@@ -19,6 +19,12 @@ using namespace Runtime;

namespace eosio { namespace chain {

struct account_module {
digest_type digest;
std::shared_ptr<wasm_instantiated_module_interface> module_interface;
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem to be used anymore, meaning the map<account_name, account_module> account_module_map can probably become a map<account_name, digest_type>

That would remove the need to change the std::unique_ptr to a std::shared_ptr as well which is nicer.


auto wasm = ::eosio::chain::wast_to_wasm(code);

auto wasm_code = shared_vector<char>(alloc);
Copy link
Contributor

Choose a reason for hiding this comment

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

shared_vector is just a vector that has a different allocator. For the purposes of your test it could just be a vector<char> which would make the inclusion of bip::managed_mapped_file seg(bip::open_or_create,"./test.db", 1024*1024); and all the related code to derive alloc unnecessary

code += "(call $test (call $i64_trunc_u_f64 (f64.const 1)))\n";
code += "))";

wasm_interface_impl wasm_imp(wasm_interface::vm_type::binaryen);
Copy link
Contributor

Choose a reason for hiding this comment

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

this needs to be the runtime that the unit tests are targeting

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 4, 2018 via email

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 4, 2018 via email

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 4, 2018 via email

@wanderingbort
Copy link
Contributor

module_interface in account_module is used for reference counting, so when no account reference to the code anymore,

This is true however, you never access account_module::module_instance from an iterator or entry in the account_module_map as far as I can tell.

I can only find an access for account_module::digest which is used to find the entry instantiation_cache, decrement the reference count and potentially erase the entry.

Basically, the mechanism is sound but I don't see a reason to share ownership of wasm_instantiated_module_interface between the two maps. Only one is ever used to access that data and therefore it seems like this mechanism would still work as designed with instantiation_cache having the only ownership and using std::unique_ptr

@wanderingbort
Copy link
Contributor

wasm_interface_impl::get_instantiated_module has shared_vector as it’s parameter,

Good catch, I did not realize that. I think its cleaner to have the storage semantics defined elsewhere. In the past, we've other places we've passed const char* buf, size_t buf_size in place of a shared_vector<char> and then made the caller extract those values from their preferred storage class before calling the method.

If you want to take that approach, I would approve of it. It adds a small amount of complexity to the caller and removes a large amount of complexity from your test code and all the called code.

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 4, 2018 via email

@wanderingbort
Copy link
Contributor

What I missed was that you were using std::shared_ptr as a basic reference count. This is a very opaque way of achieving what you want and has the downside of implying that there is shared ownership of the module when there is not.

I would recommend making the fact that you are reference counting explicit since you don't actually need shared memory ownership.

consider this patch: https://gist.github.com/wanderingbort/117d0d21b0affab98d9216f33174ac18
This is an example using std::pair that passes your test and makes the mechanism clearer in my opinion without granting shared ownership over the lifecycle of the allocated memory.

That said, if you integrate this patch I would make an explicit struct instead of using std::pair but I wanted to make my point as quickly as possible.

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 4, 2018 via email

@spoonincode
Copy link
Contributor

spoonincode commented May 9, 2018

I think after some internal discussion a couple days ago we decided this was not what we wanted to do. Two reasons: 1) forks could throb cache invalidation more than desired and 2) we actually need the cache to additionally be aware of the code's "injection version"

@learnforpractice
Copy link
Contributor Author

learnforpractice commented May 10, 2018 via email

@gleehokie
Copy link
Contributor

@spoonincode should we close this PR?

@bytemaster
Copy link
Contributor

I would like to revisit this issue. Fork thrashing could be resolved by expiring cache on IRB rather than immediately upon changing of code. Lets resolve the merge conflicts.

@spoonincode
Copy link
Contributor

I wonder if the garbage collector in WAVM would need to be fired for the WAVM modules to actually be freed.

@learnforpractice
Copy link
Contributor Author

Sorry, I deleted my eos fork accidentally, Hope this issue will be solved soon.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants