Skip to content

Commit

Permalink
pytest: refactor mocknet load_testing_helper to track the nonce local…
Browse files Browse the repository at this point in the history
…ly (#3103)

Previously, load tests would query the nodes for the account nonce on each transaction. This put an additional burden on the nodes which is likely unrealistic. Here, we introduce an `Account` object to track the nonce locally in the test, and thus not query the nodes. Additionally, the `Account` object tracks the timestamps of when it sends transactions, allowing for detailed measurements of input transactions per second (though we do not yet make use of this feature in this change).

Note: this does not change the result of running the load test (i.e. tps and bps are not effected by this change), so the nonce querying likely had little impact on the node performance. However, I would still like to make this change since `Account` is a useful, reusable object.
  • Loading branch information
birchmd authored Aug 7, 2020
1 parent 9ca3117 commit b26a0da
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 67 deletions.
75 changes: 75 additions & 0 deletions pytest/lib/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import base64
import json
import requests
import time

from cluster import Key
from transaction import (
sign_payment_tx, sign_deploy_contract_tx, sign_function_call_tx,
sign_create_account_with_full_access_key_and_balance_tx, sign_staking_tx)
import utils


class Account:

def __init__(self, key, init_nonce, base_block_hash, rpc_info):
self.key = key
self.nonce = init_nonce
self.base_block_hash = base_block_hash
self.rpc_addr, self.rpc_port = rpc_info
self.tx_timestamps = []

def json_rpc(self, method, params):
j = {
'method': method,
'params': params,
'id': 'dontcare',
'jsonrpc': '2.0'
}
r = requests.post(f'http://{self.rpc_addr}:{self.rpc_port}',
json=j,
timeout=10)
return json.loads(r.content)

def send_tx(self, signed_tx):
return self.json_rpc('broadcast_tx_async',
[base64.b64encode(signed_tx).decode('utf8')])

def prep_tx(self):
self.tx_timestamps.append(time.time())
self.nonce += 1

def send_transfer_tx(self, dest_account_id):
self.prep_tx()
transfer_amount = 100
tx = sign_payment_tx(self.key, dest_account_id, transfer_amount,
self.nonce, self.base_block_hash)
return self.send_tx(tx)

def send_deploy_contract_tx(self, wasm_filename):
wasm_binary = utils.load_binary_file(wasm_filename)
self.prep_tx()
tx = sign_deploy_contract_tx(self.key, wasm_binary, self.nonce,
self.base_block_hash)
return self.send_tx(tx)

def send_call_contract_tx(self, method_name, args):
self.prep_tx()
tx = sign_function_call_tx(self.key, self.key.account_id, method_name,
args, 300000000000000, 0, self.nonce,
self.base_block_hash)
return self.send_tx(tx)

def send_create_account_tx(self, new_account_id):
self.prep_tx()
new_key = Key(new_account_id, self.key.pk, self.key.sk)
tx = sign_create_account_with_full_access_key_and_balance_tx(
self.key, new_account_id, new_key, 100, self.nonce,
self.base_block_hash)
return self.send_tx(tx)

def send_stake_tx(self, stake_amount):
self.prep_tx()
tx = sign_staking_tx(self.key, self.key, stake_amount, self.nonce,
self.base_block_hash)
return self.send_tx(tx)
99 changes: 32 additions & 67 deletions pytest/tests/mocknet/load_testing_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@
sys.path.append('lib')
from cluster import Key
from mocknet import NUM_NODES
from transaction import (
sign_payment_tx, sign_deploy_contract_tx, sign_function_call_tx,
sign_create_account_with_full_access_key_and_balance_tx, sign_staking_tx)
import utils
from account import Account

LOCAL_ADDR = '127.0.0.1'
RPC_PORT = '3030'
Expand Down Expand Up @@ -60,84 +57,46 @@ def get_nonce_for_pk(account_id, pk, finality='optimistic'):
return k['access_key']['nonce']


def send_tx(signed_tx):
json_rpc('broadcast_tx_async', [base64.b64encode(signed_tx).decode('utf8')])


def get_latest_block_hash():
last_block_hash = get_status()['sync_info']['latest_block_hash']
return base58.b58decode(last_block_hash.encode('utf8'))


def send_transfer(source_account, dest_index, base_block_hash):
alice = source_account
bob = load_testing_account_id(dest_index)
alice_nonce = get_nonce_for_pk(alice.account_id, alice.pk)
tranfer_amount = 100
tx = sign_payment_tx(alice, bob, tranfer_amount, alice_nonce + 1,
base_block_hash)
send_tx(tx)


def deploy_contract(source_account, base_block_hash):
nonce = get_nonce_for_pk(source_account.account_id, source_account.pk)
wasm_binary = utils.load_binary_file(WASM_FILENAME)
tx = sign_deploy_contract_tx(source_account, wasm_binary, nonce + 1,
base_block_hash)
send_tx(tx)


def call_contract(source_account, base_block_hash):
nonce = get_nonce_for_pk(source_account.account_id, source_account.pk)
tx = sign_function_call_tx(source_account, source_account.account_id,
'do_work', [], 300000000000000, 0, nonce + 1,
base_block_hash)
send_tx(tx)
def send_transfer(account, i, i0):
next_id = i + 1
if next_id >= i0 + NUM_ACCOUNTS:
next_id = i0
dest_account_id = load_testing_account_id(next_id)
account.send_transfer_tx(dest_account_id)


def create_account(source_account, base_block_hash):
nonce = get_nonce_for_pk(source_account.account_id, source_account.pk)
new_account_id = ''.join(
random.choice(string.ascii_lowercase) for _ in range(0, 10))
new_key = Key(new_account_id, source_account.pk, source_account.sk)
tx = sign_create_account_with_full_access_key_and_balance_tx(
source_account, new_account_id, new_key, 100, nonce + 1,
base_block_hash)
send_tx(tx)


def stake(source_account, base_block_hash):
nonce = get_nonce_for_pk(source_account.account_id, source_account.pk)
tx = sign_staking_tx(source_account, source_account, 1, nonce + 1,
base_block_hash)
send_tx(tx)


def random_transaction(account_and_index, base_block_hash):
def random_transaction(account_and_index, i0):
choice = random.randint(0, 3)
if choice == 0:
send_transfer(account_and_index[0], account_and_index[1] + 1, base_block_hash)
send_transfer(account_and_index[0], account_and_index[1], i0)
elif choice == 1:
call_contract(account_and_index[0], base_block_hash)
account_and_index[0].send_call_contract_tx('do_work', [])
elif choice == 2:
create_account(account_and_index[0], base_block_hash)
new_account_id = ''.join(
random.choice(string.ascii_lowercase) for _ in range(0, 10))
account_and_index[0].send_create_account_tx(new_account_id)
elif choice == 3:
stake(account_and_index[0], base_block_hash)
account_and_index[0].send_stake_tx(1)


def send_transfers(base_block_hash):
def send_transfers(i0):
pmap(
lambda account_and_index: send_transfer(account_and_index[0], (
account_and_index[1] + 1) % NUM_ACCOUNTS, base_block_hash), test_accounts)
lambda account_and_index: send_transfer(account_and_index[
0], account_and_index[1], i0), test_accounts)


def send_random_transactions(base_block_hash):
pmap(lambda x: random_transaction(x, base_block_hash), test_accounts)
def send_random_transactions(i0):
pmap(lambda x: random_transaction(x, i0), test_accounts)


def throttle_txns(send_txns, total_tx_sent, elapsed_time, max_tps, base_block_hash):
def throttle_txns(send_txns, total_tx_sent, elapsed_time, max_tps, i0):
start_time = time.time()
send_txns(base_block_hash)
send_txns(i0)
duration = time.time() - start_time
total_tx_sent += NUM_ACCOUNTS
elapsed_time += duration
Expand All @@ -156,14 +115,20 @@ def throttle_txns(send_txns, total_tx_sent, elapsed_time, max_tps, base_block_ha
pk = sys.argv[2]
sk = sys.argv[3]

test_accounts = [
test_account_keys = [
(Key(load_testing_account_id(i), pk, sk), i)
for i in range(node_index * NUM_ACCOUNTS, (node_index + 1) *
NUM_ACCOUNTS)
]

base_block_hash = get_latest_block_hash()
rpc_info = (LOCAL_ADDR, RPC_PORT)

test_accounts = [(Account(key, get_nonce_for_pk(key.account_id, key.pk),
base_block_hash, rpc_info), i)
for (key, i) in test_account_keys]

i0 = test_accounts[0][1]
start_time = time.time()

# begin with only transfers for TPS measurement
Expand All @@ -172,19 +137,19 @@ def throttle_txns(send_txns, total_tx_sent, elapsed_time, max_tps, base_block_ha
while time.time() - start_time < TRANSFER_ONLY_TIMEOUT:
(total_tx_sent,
elapsed_time) = throttle_txns(send_transfers, total_tx_sent,
elapsed_time, MAX_TPS_PER_NODE, base_block_hash)
elapsed_time, MAX_TPS_PER_NODE, i0)

# Ensure load testing contract is deployed to all accounts before
# starting to send random transactions (ensures we do not try to
# call the contract before it is deployed).
delay = CONTRACT_DEPLOY_TIME / NUM_ACCOUNTS
for x in test_accounts:
deploy_contract(x[0], base_block_hash)
for (account, _) in test_accounts:
account.send_deploy_contract_tx(WASM_FILENAME)
time.sleep(delay)

# send all sorts of transactions
start_time = time.time()
while time.time() - start_time < ALL_TX_TIMEOUT:
(total_tx_sent,
elapsed_time) = throttle_txns(send_random_transactions, total_tx_sent,
elapsed_time, MAX_TPS_PER_NODE, base_block_hash)
elapsed_time, MAX_TPS_PER_NODE, i0)

0 comments on commit b26a0da

Please sign in to comment.