wallet server sync performance improvements

This commit is contained in:
Lex Berezhny 2019-05-14 23:59:21 -04:00
parent 03a1185e0f
commit 5f4c02f836
3 changed files with 117 additions and 78 deletions

View file

@ -49,7 +49,7 @@ class Timer:
print('='*100) print('='*100)
else: else:
print( print(
f"{' '*depth} {self.total/60:.2f}mins {self.name}" f"{' '*depth} {self.total/60:4.2f}mins {self.name}"
# f"{self.total/self.count:.5f}sec/call, " # f"{self.total/self.count:.5f}sec/call, "
) )
for sub_timer in self.sub_timers.values(): for sub_timer in self.sub_timers.values():
@ -83,7 +83,7 @@ class LBRYBlockProcessor(BlockProcessor):
timer = self.timer.sub_timers['advance_blocks'] timer = self.timer.sub_timers['advance_blocks']
undo = timer.run(super().advance_txs, height, txs, timer_name='super().advance_txs') undo = timer.run(super().advance_txs, height, txs, timer_name='super().advance_txs')
timer.run(self.sql.advance_txs, height, txs, forward_timer=True) timer.run(self.sql.advance_txs, height, txs, forward_timer=True)
if height % 20000 == 0: if height % 10000 == 0:
self.timer.show(height=height) self.timer.show(height=height)
return undo return undo

View file

@ -75,6 +75,7 @@ class SQLDB:
channel_hash bytes, channel_hash bytes,
activation_height integer, activation_height integer,
effective_amount integer not null default 0, effective_amount integer not null default 0,
support_amount integer not null default 0,
trending_amount integer not null default 0 trending_amount integer not null default 0
); );
create index if not exists claim_normalized_idx on claim (normalized); create index if not exists claim_normalized_idx on claim (normalized);
@ -98,11 +99,11 @@ class SQLDB:
CREATE_TAG_TABLE = """ CREATE_TAG_TABLE = """
create table if not exists tag ( create table if not exists tag (
tag text not null, tag text not null,
txo_hash bytes not null, claim_hash bytes not null,
height integer not null height integer not null
); );
create index if not exists tag_tag_idx on tag (tag); create index if not exists tag_tag_idx on tag (tag);
create index if not exists tag_txo_hash_idx on tag (txo_hash); create index if not exists tag_claim_hash_idx on tag (claim_hash);
create index if not exists tag_height_idx on tag (height); create index if not exists tag_height_idx on tag (height);
""" """
@ -123,7 +124,8 @@ class SQLDB:
CREATE_TAG_TABLE CREATE_TAG_TABLE
) )
def __init__(self, path): def __init__(self, main, path):
self.main = main
self._db_path = path self._db_path = path
self.db = None self.db = None
self.logger = class_logger(__name__, self.__class__.__name__) self.logger = class_logger(__name__, self.__class__.__name__)
@ -172,8 +174,8 @@ class SQLDB:
def commit(self): def commit(self):
self.execute('commit;') self.execute('commit;')
def _upsertable_claims(self, txos: Set[Output]): def _upsertable_claims(self, txos: Set[Output], clear_first=False):
claims, tags = [], [] claim_hashes, claims, tags = [], [], []
for txo in txos: for txo in txos:
tx = txo.tx_ref.tx tx = txo.tx_ref.tx
@ -184,13 +186,14 @@ class SQLDB:
#self.logger.exception(f"Could not decode claim name for {tx.id}:{txo.position}.") #self.logger.exception(f"Could not decode claim name for {tx.id}:{txo.position}.")
continue continue
txo_hash = sqlite3.Binary(txo.ref.hash) claim_hash = sqlite3.Binary(txo.claim_hash)
claim_hashes.append(claim_hash)
claim_record = { claim_record = {
'claim_hash': sqlite3.Binary(txo.claim_hash), 'claim_hash': claim_hash,
'normalized': txo.normalized_name, 'normalized': txo.normalized_name,
'claim_name': txo.claim_name, 'claim_name': txo.claim_name,
'is_channel': False, 'is_channel': False,
'txo_hash': txo_hash, 'txo_hash': sqlite3.Binary(txo.ref.hash),
'tx_position': tx.position, 'tx_position': tx.position,
'height': tx.height, 'height': tx.height,
'amount': txo.amount, 'amount': txo.amount,
@ -208,11 +211,14 @@ class SQLDB:
if claim.signing_channel_hash: if claim.signing_channel_hash:
claim_record['channel_hash'] = sqlite3.Binary(claim.signing_channel_hash) claim_record['channel_hash'] = sqlite3.Binary(claim.signing_channel_hash)
for tag in claim.message.tags: for tag in claim.message.tags:
tags.append((tag, txo_hash, tx.height)) tags.append((tag, claim_hash, tx.height))
if clear_first:
self._clear_claim_metadata(claim_hashes)
if tags: if tags:
self.db.executemany( self.db.executemany(
"INSERT INTO tag (tag, txo_hash, height) VALUES (?, ?, ?)", tags "INSERT INTO tag (tag, claim_hash, height) VALUES (?, ?, ?)", tags
) )
return claims return claims
@ -231,7 +237,7 @@ class SQLDB:
) )
def update_claims(self, txos: Set[Output]): def update_claims(self, txos: Set[Output]):
claims = self._upsertable_claims(txos) claims = self._upsertable_claims(txos, clear_first=True)
if claims: if claims:
self.db.executemany( self.db.executemany(
"UPDATE claim SET " "UPDATE claim SET "
@ -241,27 +247,34 @@ class SQLDB:
claims claims
) )
def clear_claim_metadata(self, txo_hashes: Set[bytes]): def delete_claims(self, claim_hashes: Set[bytes]):
""" Deletes metadata associated with claim in case of an update or an abandon. """
if txo_hashes:
binary_txo_hashes = [sqlite3.Binary(txo_hash) for txo_hash in txo_hashes]
for table in ('tag',): # 'language', 'location', etc
self.execute(*self._delete_sql(table, {'txo_hash__in': binary_txo_hashes}))
def abandon_claims(self, claim_hashes: Set[bytes]):
""" Deletes claim supports and from claimtrie in case of an abandon. """ """ Deletes claim supports and from claimtrie in case of an abandon. """
if claim_hashes: if claim_hashes:
binary_claim_hashes = [sqlite3.Binary(claim_hash) for claim_hash in claim_hashes] binary_claim_hashes = [sqlite3.Binary(claim_hash) for claim_hash in claim_hashes]
for table in ('claim', 'support', 'claimtrie'): for table in ('claim', 'support', 'claimtrie'):
self.execute(*self._delete_sql(table, {'claim_hash__in': binary_claim_hashes})) self.execute(*self._delete_sql(table, {'claim_hash__in': binary_claim_hashes}))
self._clear_claim_metadata(binary_claim_hashes)
def split_inputs_into_claims_and_other(self, txis): def _clear_claim_metadata(self, binary_claim_hashes: List[sqlite3.Binary]):
all = set(txi.txo_ref.hash for txi in txis) if binary_claim_hashes:
for table in ('tag',): # 'language', 'location', etc
self.execute(*self._delete_sql(table, {'claim_hash__in': binary_claim_hashes}))
def split_inputs_into_claims_supports_and_other(self, txis):
txo_hashes = set(txi.txo_ref.hash for txi in txis)
claims = dict(self.execute(*query( claims = dict(self.execute(*query(
"SELECT txo_hash, claim_hash FROM claim", "SELECT txo_hash, claim_hash FROM claim",
txo_hash__in=[sqlite3.Binary(txo_hash) for txo_hash in all] txo_hash__in=[sqlite3.Binary(txo_hash) for txo_hash in txo_hashes]
))) )))
return claims, all-set(claims) txo_hashes -= set(claims)
supports = {}
if txo_hashes:
supports = dict(self.execute(*query(
"SELECT txo_hash, claim_hash FROM support",
txo_hash__in=[sqlite3.Binary(txo_hash) for txo_hash in txo_hashes]
)))
txo_hashes -= set(supports)
return claims, supports, txo_hashes
def insert_supports(self, txos: Set[Output]): def insert_supports(self, txos: Set[Output]):
supports = [] supports = []
@ -279,7 +292,7 @@ class SQLDB:
"VALUES (?, ?, ?, ?, ?)", supports "VALUES (?, ?, ?, ?, ?)", supports
) )
def delete_other_txos(self, txo_hashes: Set[bytes]): def delete_supports(self, txo_hashes: Set[bytes]):
if txo_hashes: if txo_hashes:
self.execute(*self._delete_sql( self.execute(*self._delete_sql(
'support', {'txo_hash__in': [sqlite3.Binary(txo_hash) for txo_hash in txo_hashes]} 'support', {'txo_hash__in': [sqlite3.Binary(txo_hash) for txo_hash in txo_hashes]}
@ -299,26 +312,6 @@ class SQLDB:
SELECT claim_hash FROM claimtrie SELECT claim_hash FROM claimtrie
) )
""") """)
def _update_trending_amount(self, height):
self.execute(f"""
UPDATE claim SET
trending_amount = COALESCE(
(SELECT SUM(amount) FROM support WHERE support.claim_hash=claim.claim_hash
AND support.height > {height-self.TRENDING_BLOCKS}), 0
)
""")
def _update_effective_amount(self, height):
self.execute(f"""
UPDATE claim SET
effective_amount = claim.amount + COALESCE(
(SELECT SUM(amount) FROM support WHERE support.claim_hash=claim.claim_hash), 0
)
WHERE activation_height <= {height}
""")
def _set_activation_height(self, height):
self.execute(f""" self.execute(f"""
UPDATE claim SET UPDATE claim SET
activation_height = {height} + min(4032, cast( activation_height = {height} + min(4032, cast(
@ -330,6 +323,38 @@ class SQLDB:
WHERE activation_height IS NULL WHERE activation_height IS NULL
""") """)
def _update_trending_amount(self, height):
self.execute(f"""
UPDATE claim SET
trending_amount = COALESCE(
(SELECT SUM(amount) FROM support WHERE support.claim_hash=claim.claim_hash
AND support.height > {height-self.TRENDING_BLOCKS}), 0
)
""")
def _update_support_amount(self, claim_hashes):
if claim_hashes:
self.execute(f"""
UPDATE claim SET
support_amount = COALESCE(
(SELECT SUM(amount) FROM support WHERE support.claim_hash=claim.claim_hash), 0
)
WHERE claim_hash IN ({','.join('?' for _ in claim_hashes)})
""", [sqlite3.Binary(claim_hash) for claim_hash in claim_hashes])
def _update_effective_amount(self, height, claim_hashes=None):
sql = f"""
UPDATE claim SET effective_amount = claim.amount + claim.support_amount
WHERE activation_height = {height}
"""
if claim_hashes:
self.execute(
f"{sql} OR (activation_height <= {height} AND claim_hash IN ({','.join('?' for _ in claim_hashes)}))",
[sqlite3.Binary(claim_hash) for claim_hash in claim_hashes]
)
else:
self.execute(sql)
def get_overtakings(self): def get_overtakings(self):
return self.execute(f""" return self.execute(f"""
SELECT winner.normalized, winner.claim_hash FROM ( SELECT winner.normalized, winner.claim_hash FROM (
@ -351,12 +376,13 @@ class SQLDB:
(sqlite3.Binary(overtake['claim_hash']), overtake['normalized']) (sqlite3.Binary(overtake['claim_hash']), overtake['normalized'])
) )
def update_claimtrie(self, height, timer): def update_claimtrie(self, height, removed_claims, new_claims, recalc_claims, timer):
r = timer.run r = timer.run
r(self._make_claims_without_competition_become_controlling, height) r(self._make_claims_without_competition_become_controlling, height)
r(self._update_trending_amount, height) r(self._update_support_amount, recalc_claims)
r(self._update_effective_amount, height) r(self._update_effective_amount, height, recalc_claims)
r(self._set_activation_height, height) if not self.main.first_sync:
r(self._update_trending_amount, height)
r(self._perform_overtake, height) r(self._perform_overtake, height)
r(self._update_effective_amount, height) r(self._update_effective_amount, height)
r(self._perform_overtake, height) r(self._perform_overtake, height)
@ -488,49 +514,54 @@ class SQLDB:
return result return result
def advance_txs(self, height, all_txs, timer): def advance_txs(self, height, all_txs, timer):
sql, txs = self, set() body_timer = timer.add_timer('body')
abandon_claim_hashes, stale_claim_metadata_txo_hashes = set(), set() body_timer.start()
insert_claims, update_claims = set(), set() insert_claims = set()
delete_txo_hashes, insert_supports = set(), set() update_claims = set()
delete_claims = set()
recalc_claims = set()
insert_supports = set()
delete_supports = set()
for position, (etx, txid) in enumerate(all_txs): for position, (etx, txid) in enumerate(all_txs):
body_timer.stop()
tx = timer.run( tx = timer.run(
Transaction, etx.serialize(), height=height, position=position Transaction, etx.serialize(), height=height, position=position
) )
claim_abandon_map, delete_txo_hashes = timer.run( spent_claims, spent_supports, spent_other = timer.run(
sql.split_inputs_into_claims_and_other, tx.inputs self.split_inputs_into_claims_supports_and_other, tx.inputs
) )
stale_claim_metadata_txo_hashes.update(claim_abandon_map) body_timer.start()
delete_claims.update(spent_claims.values())
recalc_claims.update(spent_supports.values())
delete_supports.update(spent_supports)
for output in tx.outputs: for output in tx.outputs:
if output.is_support: if output.is_support:
txs.add(tx)
insert_supports.add(output) insert_supports.add(output)
recalc_claims.add(output.claim_hash)
elif output.script.is_claim_name: elif output.script.is_claim_name:
txs.add(tx)
insert_claims.add(output) insert_claims.add(output)
recalc_claims.add(output.claim_hash)
elif output.script.is_update_claim: elif output.script.is_update_claim:
txs.add(tx) claim_hash = output.claim_hash
if claim_hash in delete_claims:
delete_claims.remove(claim_hash)
update_claims.add(output) update_claims.add(output)
# don't abandon update claims (removes supports & removes from claimtrie) recalc_claims.add(claim_hash)
for txo_hash, input_claim_hash in claim_abandon_map.items(): body_timer.stop()
if output.claim_hash == input_claim_hash:
del claim_abandon_map[txo_hash]
break
abandon_claim_hashes.update(claim_abandon_map.values())
r = timer.run r = timer.run
r(sql.abandon_claims, abandon_claim_hashes) r(self.delete_claims, delete_claims)
r(sql.clear_claim_metadata, stale_claim_metadata_txo_hashes) r(self.delete_supports, delete_supports)
r(sql.delete_other_txos, delete_txo_hashes) r(self.insert_claims, insert_claims)
r(sql.insert_claims, insert_claims) r(self.update_claims, update_claims)
r(sql.update_claims, update_claims) r(self.insert_supports, insert_supports)
r(sql.insert_supports, insert_supports) r(self.update_claimtrie, height, delete_claims, insert_claims, recalc_claims, forward_timer=True)
r(sql.update_claimtrie, height, forward_timer=True)
class LBRYDB(DB): class LBRYDB(DB):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.sql = SQLDB('claims.db') self.sql = SQLDB(self, 'claims.db')
def close(self): def close(self):
super().close() super().close()

View file

@ -3,6 +3,7 @@ from torba.client.constants import COIN, NULL_HASH32
from lbrynet.schema.claim import Claim from lbrynet.schema.claim import Claim
from lbrynet.wallet.server.db import SQLDB from lbrynet.wallet.server.db import SQLDB
from lbrynet.wallet.server.block_processor import Timer
from lbrynet.wallet.transaction import Transaction, Input, Output from lbrynet.wallet.transaction import Transaction, Input, Output
@ -31,13 +32,17 @@ class OldWalletServerTransaction:
class TestSQLDB(unittest.TestCase): class TestSQLDB(unittest.TestCase):
def setUp(self): def setUp(self):
self.sql = SQLDB(':memory:') self.first_sync = False
self.sql = SQLDB(self, ':memory:')
self.timer = Timer('BlockProcessor')
self.sql.open() self.sql.open()
self._current_height = 0 self._current_height = 0
self._txos = {} self._txos = {}
def _make_tx(self, output): def _make_tx(self, output, txi=None):
tx = get_tx().add_outputs([output]) tx = get_tx().add_outputs([output])
if txi is not None:
tx.add_inputs([txi])
self._txos[output.ref.hash] = output self._txos[output.ref.hash] = output
return OldWalletServerTransaction(tx), tx.hash return OldWalletServerTransaction(tx), tx.hash
@ -58,7 +63,8 @@ class TestSQLDB(unittest.TestCase):
return self._make_tx( return self._make_tx(
Output.pay_update_claim_pubkey_hash( Output.pay_update_claim_pubkey_hash(
amount, claim.claim_name, claim.claim_id, claim.claim, b'abc' amount, claim.claim_name, claim.claim_id, claim.claim, b'abc'
) ),
Input.spend(claim)
) )
def get_support(self, tx, amount): def get_support(self, tx, amount):
@ -95,8 +101,10 @@ class TestSQLDB(unittest.TestCase):
return accepted return accepted
def advance(self, height, txs): def advance(self, height, txs):
#for skipped_height in range(self._current_height+1, height):
# self.sql.advance_txs(skipped_height, [], self.timer)
self._current_height = height self._current_height = height
self.sql.advance_txs(height, txs) self.sql.advance_txs(height, txs, self.timer)
def state(self, controlling=None, active=None, accepted=None): def state(self, controlling=None, active=None, accepted=None):
self.assertEqual(controlling or [], self.get_controlling()) self.assertEqual(controlling or [], self.get_controlling())