Skip to content

Commit

Permalink
[Qt][Lib] Various fixups and refactorings
Browse files Browse the repository at this point in the history
- More cleanup of the recent 'verifier' GUI speedups. Got rid of the
signal/slot mechanism for propagating the history-list-got-a-verified-tx
condition and instead we do it using an approach using less overhead

- Got rid of the notify_transactions_signal in main_window.py -- this
signal was a bit superfluous and could just lead to Qt signal/slot spam,
taking precious cycles away from the GUI event loop. Instead, we set a
flag which gets checked in the timer_actions slot.

- Updated the @rate_limited decorator to accept a new parameter:
ts_after=True, which affects how the rate is applied and helps to
guarantee minimum dead time between collated calls.

- We now cache all QIcon instances related to the status bar (CPU cycles
were being wasted each call to update_status to re-read the PNG data
and render it to QIcon instances).

- Added timestamps to the debug output (monotonically increasing seconds
from app start)

- Added some locks around the tx_notifications and tx_verifications
queues in the GUI since they are shared between the network and GUI threads.

- Fixed a GUI bug where the Seed Dialog allowed users to edit the seed text
(this makes no sense since the seed is immutable) -- made the text edit
read-only for the seed dialog.

- More speedups and fixups to the verifier.

- Added the text '[verifying %d TXs...]' to the status bar when the
verifier has more than 10 tx's it is working on verifying (helps
indicate to users that progress is being made on verify for huge
wallets).
  • Loading branch information
cculianu committed Feb 13, 2019
1 parent 0bb3217 commit cf34278
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 97 deletions.
2 changes: 1 addition & 1 deletion electron-cash
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# -*- mode: python -*-
# -*- mode: python3 -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
Expand Down
2 changes: 1 addition & 1 deletion gui/qt/address_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def refresh_headers(self):
headers.insert(4, '{} {}'.format(fx.get_currency(), _(' Balance')))
self.update_headers(headers)

@rate_limited(1.0) # We rate limit the address list refresh no more than once every second
@rate_limited(1.0, ts_after=True) # We rate limit the address list refresh no more than once every second
def update(self):
if self.wallet and (not self.wallet.thread or not self.wallet.thread.isRunning()):
# short-cut return if window was closed and wallet is stopped
Expand Down
2 changes: 1 addition & 1 deletion gui/qt/history_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def get_domain(self):
'''Replaced in address_dialog.py'''
return self.wallet.get_addresses()

@rate_limited(1.0, classlevel=True) # We rate limit the history list refresh no more than once every second, app-wide
@rate_limited(1.0, classlevel=True, ts_after=True) # We rate limit the history list refresh no more than once every second, app-wide
def update(self):
if self.wallet and (not self.wallet.thread or not self.wallet.thread.isRunning()):
# short-cut return if window was closed and wallet is stopped
Expand Down
178 changes: 113 additions & 65 deletions gui/qt/main_window.py

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions gui/qt/seed_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,14 @@ def f(b):
self.is_bip39 = cb_bip39.isChecked() if 'bip39' in self.options else False
self.is_bip39_145 = cb_bip39_145.isChecked() if 'bip39_145' in self.options else False

def __init__(self, seed=None, title=None, icon=True, msg=None, options=None, is_seed=None, passphrase=None, parent=None):
def __init__(self, seed=None, title=None, icon=True, msg=None, options=None, is_seed=None, passphrase=None, parent=None, editable=True):
QVBoxLayout.__init__(self)
self.parent = parent
self.options = options
if title:
self.addWidget(WWLabel(title))
self.seed_e = ButtonsTextEdit()
self.seed_e.setReadOnly(not editable)
if seed:
self.seed_e.setText(seed)
else:
Expand Down Expand Up @@ -204,6 +205,6 @@ def __init__(self, parent, seed, passphrase):
self.setMinimumWidth(400)
vbox = QVBoxLayout(self)
title = _("Your wallet generation seed is:")
slayout = SeedLayout(title=title, seed=seed, msg=True, passphrase=passphrase)
slayout = SeedLayout(title=title, seed=seed, msg=True, passphrase=passphrase, editable=False)
vbox.addLayout(slayout)
vbox.addLayout(Buttons(CloseButton(self)))
53 changes: 38 additions & 15 deletions gui/qt/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,10 +681,11 @@ class RateLimiter(PrintError):
saved_args = (tuple(),dict())
ctr = 0

def __init__(self, rate, obj, func):
def __init__(self, rate, ts_after, obj, func):
self.n = func.__name__
self.qn = func.__qualname__
self.rate = rate
self.ts_after = ts_after
self.obj = Weak.ref(obj) # keep a weak reference to the object to prevent cycles
self.func = func
#self.print_error("*** Created: func=",func,"obj=",obj,"rate=",rate)
Expand All @@ -703,7 +704,7 @@ def kill_timer(self):
def attr_name(cls, func): return "__{}__{}".format(func.__name__, cls.__name__)

@classmethod
def invoke(cls, rate, func, args, kwargs):
def invoke(cls, rate, ts_after, func, args, kwargs):
''' Calls _invoke() on an existing RateLimiter object (or creates a new
one for the given function on first run per target object instance). '''
assert args and isinstance(args[0], object), "@rate_limited decorator may only be used with object instance methods"
Expand All @@ -714,7 +715,7 @@ def invoke(cls, rate, func, args, kwargs):
rl = getattr(obj, a_name, None) # we hide the RateLimiter state object in an attribute (name based on the wrapped function name) in the target object
if rl is None:
# must be the first invocation, create a new RateLimiter state instance.
rl = cls(rate, obj, func)
rl = cls(rate, ts_after, obj, func)
setattr(obj, a_name, rl)
return rl._invoke(args, kwargs)

Expand Down Expand Up @@ -762,11 +763,14 @@ def _doIt(self):
del args, kwargs # deref args right away (allow them to get gc'd)
tf = time.time()
time_taken = tf-t0
if time_taken > float(self.rate):
self.print_error("method took too long: {} > {}. Fudging timestamps to compensate.".format(time_taken, self.rate))
self.last_ts = tf # Hmm. This function takes longer than its rate to complete. so mark its last run time as 'now'. This breaks the rate but at least prevents this function from starving the CPU (benforces a delay).
if self.ts_after:
self.last_ts = tf
else:
self.last_ts = t0 # Function takes less than rate to complete, so mark its t0 as when we entered to keep the rate constant.
if time_taken > float(self.rate):
self.print_error("method took too long: {} > {}. Fudging timestamps to compensate.".format(time_taken, self.rate))
self.last_ts = tf # Hmm. This function takes longer than its rate to complete. so mark its last run time as 'now'. This breaks the rate but at least prevents this function from starving the CPU (benforces a delay).
else:
self.last_ts = t0 # Function takes less than rate to complete, so mark its t0 as when we entered to keep the rate constant.

if self.timer: # timer is not None if and only if we were a delayed (collated) invocation.
if was_reentrant:
Expand Down Expand Up @@ -804,13 +808,13 @@ class RateLimiterClassLvl(RateLimiter):
'''

@classmethod
def invoke(cls, rate, func, args, kwargs):
def invoke(cls, rate, ts_after, func, args, kwargs):
assert args and not isinstance(args[0], type), "@rate_limited decorator may not be used with static or class methods"
obj = args[0]
objcls = obj.__class__
args = list(args)
args.insert(0, objcls) # prepend obj class to trick super.invoke() into making this state object be class-level.
return super(RateLimiterClassLvl, cls).invoke(rate, func, args, kwargs)
return super(RateLimiterClassLvl, cls).invoke(rate, ts_after, func, args, kwargs)

def _push_args(self, args, kwargs):
objcls, obj = args[0:2]
Expand All @@ -830,29 +834,48 @@ def _call_func_for_all(self, weak_dict):
#self.print_error("calling for",obj.diagnostic_name() if hasattr(obj, "diagnostic_name") else obj,"timer=",bool(self.timer))
self.func_target(obj, *args, **kwargs)

def __init__(self,rate, obj, func):
def __init__(self, rate, ts_after, obj, func):
# note: obj here is really the __class__ of the obj because we prepended the class in our custom invoke() above.
super().__init__(rate, obj, func)
super().__init__(rate, ts_after, obj, func)
self.func_target = func
self.func = self._call_func_for_all
self.saved_args = Weak.KeyDictionary() # we don't use a simple arg tuple, but instead an instance -> args,kwargs dictionary to store collated calls, per instance collated


def rate_limited(rate, classlevel=False):
def rate_limited(rate, *, classlevel=False, ts_after=False):
""" A Function decorator for rate-limiting GUI event callbacks. Argument
rate in seconds is the minimum allowed time between subsequent calls of
this instance of the function. Calls that arrive more frequently than
rate seconds will be collated into a single call that is deferred onto
a QTimer. It is preferable to use this decorator on QObject subclass
instance methods. This decorator is particularly useful in limiting
frequent calls to GUI update functions.
(See on_fx_quotes and on_fx_history in main_window.py for an example). """
params:
rate - calls are collated to not exceed rate (in seconds)
classlevel - if True, specify that the calls should be collated at
1 per `rate` secs. for *all* instances of a class, otherwise
calls will be collated on a per-instance basis.
ts_after - if True, mark the timestamp of the 'last call' AFTER the
target method completes. That is, the collation of calls will
ensure at least `rate` seconds will always elapse between
subsequent calls. If False, the timestamp is taken right before
the collated calls execute (thus ensuring a fixed period for
collated calls).
TL;DR: ts_after=True : `rate` defines the time interval you want
from last call's exit to entry into next
call.
ts_adter=False: `rate` defines the time between each
call's entry.
(See on_fx_quotes & on_fx_history in main_window.py for example usages
of this decorator). """
def wrapper0(func):
@wraps(func)
def wrapper(*args, **kwargs):
if classlevel:
return RateLimiterClassLvl.invoke(rate, func, args, kwargs)
return RateLimiter.invoke(rate, func, args, kwargs)
return RateLimiterClassLvl.invoke(rate, ts_after, func, args, kwargs)
return RateLimiter.invoke(rate, ts_after, func, args, kwargs)
return wrapper
return wrapper0

Expand Down
2 changes: 1 addition & 1 deletion gui/qt/utxo_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def get_name(self, x):
def get_name_short(self, x):
return x.get('prevout_hash')[:10] + '...' + ":%d"%x.get('prevout_n')

@rate_limited(1.0) # performance tweak -- limit updates to no more than oncer per second
@rate_limited(1.0, ts_after=True) # performance tweak -- limit updates to no more than oncer per second
def update(self):
if self.wallet and (not self.wallet.thread or not self.wallet.thread.isRunning()):
# short-cut return if window was closed and wallet is stopped
Expand Down
2 changes: 1 addition & 1 deletion ios/ElectronCash/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def main():
'cwd': os.getcwd(),
}

set_verbosity(config_options.get('verbose'))
set_verbosity(config_options.get('verbose'), timestamps=False)
NSLogSuppress(not config_options.get('verbose'))

MonkeyPatches.patch()
Expand Down
9 changes: 7 additions & 2 deletions lib/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,11 @@ def on_stop(self):

# TODO: disable
is_verbose = True
def set_verbosity(b):
global is_verbose
verbose_timestamps = True
def set_verbosity(b, *, timestamps=True):
global is_verbose, verbose_timestamps
is_verbose = b
verbose_timestamps = timestamps

# Method decorator. To be used for calculations that will always
# deliver the same result. The method cannot take any arguments
Expand All @@ -201,8 +203,11 @@ def __get__(self, obj, type):
setattr(obj, self.f.__name__, value)
return value

_t0 = time.time()
def print_error(*args):
if not is_verbose: return
if verbose_timestamps:
args = ("|%7.3f|"%(time.time() - _t0), *args)
print_stderr(*args)

def print_stderr(*args):
Expand Down
15 changes: 7 additions & 8 deletions lib/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(self, network, wallet):
self.blockchain = network.blockchain()
self.merkle_roots = {} # txid -> merkle root (once it has been verified)
self.requested_merkle = set() # txid set of pending requests
self.qbusy = False

def run(self):
interface = self.network.interface
Expand Down Expand Up @@ -66,17 +67,14 @@ def run(self):
# Also, they're not supported as header requests are
# currently designed for catching up post-checkpoint headers.
index = tx_height // 2016
if interface is not self.network.interface:
self.print_error("interface changed, will retry again later")
return
if self.network.request_chunk(interface, index):
interface.print_error("verifier requesting chunk {} for height {}".format(index, tx_height))
continue
# enqueue request
m_id = self.network.get_merkle_for_transaction(tx_hash,
tx_height,
self.verify_merkle)
if m_id is None:
msg_id = self.network.get_merkle_for_transaction(tx_hash, tx_height,
self.verify_merkle)
self.qbusy = msg_id is None
if self.qbusy:
# interface queue busy, will try again later
break
self.print_error('requested merkle', tx_hash)
Expand Down Expand Up @@ -131,7 +129,7 @@ def verify_merkle(self, response):
pass
self.print_error("verified %s" % tx_hash)
self.wallet.add_verified_tx(tx_hash, (tx_height, header.get('timestamp'), pos))
if self.is_up_to_date() and self.wallet.is_up_to_date():
if self.is_up_to_date() and self.wallet.is_up_to_date() and not self.qbusy:
self.wallet.save_verified_tx(write=True)

@classmethod
Expand Down Expand Up @@ -162,6 +160,7 @@ def undo_verifications(self):
for tx_hash in tx_hashes:
self.print_error("redoing", tx_hash)
self.remove_spv_proof_for_tx(tx_hash)
self.qbusy = False

def remove_spv_proof_for_tx(self, tx_hash):
self.merkle_roots.pop(tx_hash, None)
Expand Down

0 comments on commit cf34278

Please sign in to comment.