1
0
Files
electrum/electrum/json_db.py
Malcolm Smith 67ae678137 storage/db: use faster JSON encoder settings when wallet is encrypted
The standard json module has an optimized C encoder, but that doesn't
currently support indentation. So if you request indentation, it falls
back on the slower Python encoder.

Readability doesn't matter for encrypted wallets, so this disables
indentation when the wallet is encrypted.

-----

based on b2399b6a3e

For a large encrypted wallet, compare:
before change:
JsonDB.dump 1.3153 sec
zlib.compress 1.281 sec
ECPubkey.encrypt_message 0.1744 sec

after change:
JsonDB.dump 0.5059 sec
zlib.compress 1.3120 sec
ECPubkey.encrypt_message 0.1630 sec

Co-authored-by: SomberNight <somber.night@protonmail.com>
2021-01-06 21:14:56 +01:00

209 lines
6.2 KiB
Python

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2019 The Electrum Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import threading
import copy
import json
from . import util
from .logging import Logger
JsonDBJsonEncoder = util.MyEncoder
def modifier(func):
def wrapper(self, *args, **kwargs):
with self.lock:
self._modified = True
return func(self, *args, **kwargs)
return wrapper
def locked(func):
def wrapper(self, *args, **kwargs):
with self.lock:
return func(self, *args, **kwargs)
return wrapper
class StoredObject:
db = None
def __setattr__(self, key, value):
if self.db:
self.db.set_modified(True)
object.__setattr__(self, key, value)
def set_db(self, db):
self.db = db
def to_json(self):
d = dict(vars(self))
d.pop('db', None)
# don't expose/store private stuff
d = {k: v for k, v in d.items()
if not k.startswith('_')}
return d
_RaiseKeyError = object() # singleton for no-default behavior
class StoredDict(dict):
def __init__(self, data, db, path):
self.db = db
self.lock = self.db.lock if self.db else threading.RLock()
self.path = path
# recursively convert dicts to StoredDict
for k, v in list(data.items()):
self.__setitem__(k, v)
def convert_key(self, key):
"""Convert int keys to str keys, as only those are allowed in json."""
# NOTE: this is evil. really hard to keep in mind and reason about. :(
# e.g.: imagine setting int keys everywhere, and then iterating over the dict:
# suddenly the keys are str...
return str(int(key)) if isinstance(key, int) else key
@locked
def __setitem__(self, key, v):
key = self.convert_key(key)
is_new = key not in self
# early return to prevent unnecessary disk writes
if not is_new and self[key] == v:
return
# recursively set db and path
if isinstance(v, StoredDict):
v.db = self.db
v.path = self.path + [key]
for k, vv in v.items():
v[k] = vv
# recursively convert dict to StoredDict.
# _convert_dict is called breadth-first
elif isinstance(v, dict):
if self.db:
v = self.db._convert_dict(self.path, key, v)
if not self.db or self.db._should_convert_to_stored_dict(key):
v = StoredDict(v, self.db, self.path + [key])
# convert_value is called depth-first
if isinstance(v, dict) or isinstance(v, str):
if self.db:
v = self.db._convert_value(self.path, key, v)
# set parent of StoredObject
if isinstance(v, StoredObject):
v.set_db(self.db)
# set item
dict.__setitem__(self, key, v)
if self.db:
self.db.set_modified(True)
@locked
def __delitem__(self, key):
key = self.convert_key(key)
dict.__delitem__(self, key)
if self.db:
self.db.set_modified(True)
@locked
def __getitem__(self, key):
key = self.convert_key(key)
return dict.__getitem__(self, key)
@locked
def __contains__(self, key):
key = self.convert_key(key)
return dict.__contains__(self, key)
@locked
def pop(self, key, v=_RaiseKeyError):
key = self.convert_key(key)
if v is _RaiseKeyError:
r = dict.pop(self, key)
else:
r = dict.pop(self, key, v)
if self.db:
self.db.set_modified(True)
return r
@locked
def get(self, key, default=None):
key = self.convert_key(key)
return dict.get(self, key, default)
class JsonDB(Logger):
def __init__(self, data):
Logger.__init__(self)
self.lock = threading.RLock()
self.data = data
self._modified = False
def set_modified(self, b):
with self.lock:
self._modified = b
def modified(self):
return self._modified
@locked
def get(self, key, default=None):
v = self.data.get(key)
if v is None:
v = default
return v
@modifier
def put(self, key, value):
try:
json.dumps(key, cls=JsonDBJsonEncoder)
json.dumps(value, cls=JsonDBJsonEncoder)
except:
self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})")
return False
if value is not None:
if self.data.get(key) != value:
self.data[key] = copy.deepcopy(value)
return True
elif key in self.data:
self.data.pop(key)
return True
return False
@locked
def dump(self, *, human_readable: bool = True) -> str:
"""Serializes the DB as a string.
'human_readable': makes the json indented and sorted, but this is ~2x slower
"""
return json.dumps(
self.data,
indent=4 if human_readable else None,
sort_keys=bool(human_readable),
cls=JsonDBJsonEncoder,
)
def _should_convert_to_stored_dict(self, key) -> bool:
return True