Improve the memory() function on linux and explicitly break a few more cycles

This commit is contained in:
Kovid Goyal 2011-06-23 09:25:44 -06:00
commit de246e1cd0
5 changed files with 194 additions and 44 deletions

View File

@ -25,7 +25,7 @@ class Base(object):
def __init__(self, db, col_id, parent=None): def __init__(self, db, col_id, parent=None):
self.db, self.col_id = db, col_id self.db, self.col_id = db, col_id
self.col_metadata = db.custom_column_num_map[col_id] self.col_metadata = db.custom_column_num_map[col_id]
self.initial_val = None self.initial_val = self.widgets = None
self.setup_ui(parent) self.setup_ui(parent)
def initialize(self, book_id): def initialize(self, book_id):
@ -54,6 +54,9 @@ class Base(object):
def normalize_ui_val(self, val): def normalize_ui_val(self, val):
return val return val
def break_cycles(self):
self.db = self.widgets = self.initial_val = None
class Bool(Base): class Bool(Base):
def setup_ui(self, parent): def setup_ui(self, parent):

View File

@ -126,6 +126,9 @@ class TitleEdit(EnLineEdit):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def break_cycles(self):
self.dialog = None
class TitleSortEdit(TitleEdit): class TitleSortEdit(TitleEdit):
TITLE_ATTR = 'title_sort' TITLE_ATTR = 'title_sort'
@ -151,6 +154,7 @@ class TitleSortEdit(TitleEdit):
self.title_edit.textChanged.connect(self.update_state) self.title_edit.textChanged.connect(self.update_state)
self.textChanged.connect(self.update_state) self.textChanged.connect(self.update_state)
self.autogen_button = autogen_button
autogen_button.clicked.connect(self.auto_generate) autogen_button.clicked.connect(self.auto_generate)
self.update_state() self.update_state()
@ -169,6 +173,9 @@ class TitleSortEdit(TitleEdit):
def auto_generate(self, *args): def auto_generate(self, *args):
self.current_val = title_sort(self.title_edit.current_val) self.current_val = title_sort(self.title_edit.current_val)
self.title_edit.textChanged.disconnect()
self.textChanged.disconnect()
self.autogen_button.clicked.disconnect()
# }}} # }}}
@ -186,6 +193,7 @@ class AuthorsEdit(MultiCompleteComboBox):
self.setWhatsThis(self.TOOLTIP) self.setWhatsThis(self.TOOLTIP)
self.setEditable(True) self.setEditable(True)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)
self.manage_authors_signal = manage_authors
manage_authors.triggered.connect(self.manage_authors) manage_authors.triggered.connect(self.manage_authors)
def manage_authors(self): def manage_authors(self):
@ -270,6 +278,10 @@ class AuthorsEdit(MultiCompleteComboBox):
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def break_cycles(self):
self.db = self.dialog = None
self.manage_authors_signal.triggered.disconnect()
class AuthorSortEdit(EnLineEdit): class AuthorSortEdit(EnLineEdit):
TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' TOOLTIP = _('Specify how the author(s) of this book should be sorted. '
@ -298,6 +310,10 @@ class AuthorSortEdit(EnLineEdit):
self.authors_edit.editTextChanged.connect(self.update_state_and_val) self.authors_edit.editTextChanged.connect(self.update_state_and_val)
self.textChanged.connect(self.update_state) self.textChanged.connect(self.update_state)
self.autogen_button = autogen_button
self.copy_a_to_as_action = copy_a_to_as_action
self.copy_as_to_a_action = copy_as_to_a_action
autogen_button.clicked.connect(self.auto_generate) autogen_button.clicked.connect(self.auto_generate)
copy_a_to_as_action.triggered.connect(self.auto_generate) copy_a_to_as_action.triggered.connect(self.auto_generate)
copy_as_to_a_action.triggered.connect(self.copy_to_authors) copy_as_to_a_action.triggered.connect(self.copy_to_authors)
@ -369,6 +385,15 @@ class AuthorSortEdit(EnLineEdit):
db.set_author_sort(id_, aus, notify=False, commit=False) db.set_author_sort(id_, aus, notify=False, commit=False)
return True return True
def break_cycles(self):
self.db = None
self.authors_edit.editTextChanged.disconnect()
self.textChanged.disconnect()
self.autogen_button.clicked.disconnect()
self.copy_a_to_as_action.triggered.disconnect()
self.copy_as_to_a_action.triggered.disconnect()
self.authors_edit = None
# }}} # }}}
# Series {{{ # Series {{{
@ -428,6 +453,10 @@ class SeriesEdit(MultiCompleteComboBox):
commit=True, allow_case_change=True) commit=True, allow_case_change=True)
return True return True
def break_cycles(self):
self.dialog = None
class SeriesIndexEdit(QDoubleSpinBox): class SeriesIndexEdit(QDoubleSpinBox):
TOOLTIP = '' TOOLTIP = ''
@ -489,6 +518,11 @@ class SeriesIndexEdit(QDoubleSpinBox):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def break_cycles(self):
self.series_edit.currentIndexChanged.disconnect()
self.series_edit.editTextChanged.disconnect()
self.series_edit.lineEdit().editingFinished.disconnect()
self.db = self.series_edit = self.dialog = None
# }}} # }}}
@ -700,6 +734,8 @@ class FormatsManager(QWidget): # {{{
if old != prefs['read_file_metadata']: if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old prefs['read_file_metadata'] = old
def break_cycles(self):
self.dialog = None
# }}} # }}}
class Cover(ImageView): # {{{ class Cover(ImageView): # {{{
@ -861,6 +897,10 @@ class Cover(ImageView): # {{{
db.remove_cover(id_, notify=False, commit=False) db.remove_cover(id_, notify=False, commit=False)
return True return True
def break_cycles(self):
self.cover_changed.disconnect()
self.dialog = self._cdata = self.current_val = self.original_val = None
# }}} # }}}
class CommentsEdit(Editor): # {{{ class CommentsEdit(Editor): # {{{

View File

@ -481,6 +481,13 @@ class MetadataSingleDialogBase(ResizableDialog):
x = getattr(self, b, None) x = getattr(self, b, None)
if x is not None: if x is not None:
disconnect(x.clicked) disconnect(x.clicked)
for widget in self.basic_metadata_widgets:
bc = getattr(widget, 'break_cycles', None)
if bc is not None and callable(bc):
bc()
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.break_cycles()
# }}} # }}}
class Splitter(QSplitter): class Splitter(QSplitter):

View File

@ -627,7 +627,8 @@ class TagTreeItem(object): # {{{
except: except:
pass pass
self.parent = self.icon_state_map = self.bold_font = self.tag = \ self.parent = self.icon_state_map = self.bold_font = self.tag = \
self.icon = self.children = None self.icon = self.children = self.tooltip = \
self.py_name = self.id_set = self.category_key = None
def __str__(self): def __str__(self):
if self.type == self.ROOT: if self.type == self.ROOT:
@ -1121,7 +1122,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.search_restriction = s self.search_restriction = s
def get_node_tree(self, sort): def get_node_tree(self, sort):
old_row_map = self.row_map[:] old_row_map_len = len(self.row_map)
self.row_map = [] self.row_map = []
self.categories = {} self.categories = {}
@ -1176,7 +1177,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map.append(category) self.row_map.append(category)
self.categories[category] = tb_categories[category]['name'] self.categories[category] = tb_categories[category]['name']
if len(old_row_map) != 0 and len(old_row_map) != len(self.row_map): if old_row_map_len != 0 and old_row_map_len != len(self.row_map):
# A category has been added or removed. We must force a rebuild of # A category has been added or removed. We must force a rebuild of
# the model # the model
return None return None
@ -1367,6 +1368,9 @@ class TagsModel(QAbstractItemModel): # {{{
self.beginRemoveRows(self.createIndex(category.row(), 0, category), self.beginRemoveRows(self.createIndex(category.row(), 0, category),
start, len(child_map)-1) start, len(child_map)-1)
category.children = ctags category.children = ctags
for i in range(start, len(child_map)):
child_map[i].break_cycles()
child_map = None
self.endRemoveRows() self.endRemoveRows()
else: else:
state_map = {} state_map = {}

View File

@ -8,61 +8,157 @@ __docformat__ = 'restructuredtext en'
''' '''
Measure memory usage of the current process. Measure memory usage of the current process.
The key function is memory() which returns the current memory usage in bytes. The key function is memory() which returns the current memory usage in MB.
You can pass a number to memory and it will be subtracted from the returned You can pass a number to memory and it will be subtracted from the returned
value. value.
''' '''
import gc, os import gc, os, re
from calibre.constants import iswindows, islinux from calibre.constants import iswindows, islinux
if islinux: if islinux:
## {{{ http://code.activestate.com/recipes/286222/ (r1) # Taken, with thanks, from:
# http://wingolog.org/archives/2007/11/27/reducing-the-footprint-of-python-applications
_proc_status = '/proc/%d/status' % os.getpid() def permute(args):
ret = []
if args:
first = args.pop(0)
for y in permute(args):
for x in first:
ret.append(x + y)
else:
ret.append('')
return ret
_scale = {'kB': 1024.0, 'mB': 1024.0*1024.0, def parsed_groups(match, *types):
'KB': 1024.0, 'MB': 1024.0*1024.0} groups = match.groups()
assert len(groups) == len(types)
return tuple([type(group) for group, type in zip(groups, types)])
def _VmB(VmKey): class VMA(dict):
'''Private. def __init__(self, *args):
''' (self.start, self.end, self.perms, self.offset,
global _proc_status, _scale self.major, self.minor, self.inode, self.filename) = args
# get pseudo file /proc/<pid>/status
try:
t = open(_proc_status)
v = t.read()
t.close()
except:
return 0.0 # non-Linux?
# get VmKey line e.g. 'VmRSS: 9999 kB\n ...'
i = v.index(VmKey)
v = v[i:].split(None, 3) # whitespace
if len(v) < 3:
return 0.0 # invalid format?
# convert Vm value to bytes
return float(v[1]) * _scale[v[2]]
def parse_smaps(pid):
with open('/proc/%s/smaps'%pid, 'r') as maps:
hex = lambda s: int(s, 16)
ret = []
header = re.compile(r'^([0-9a-f]+)-([0-9a-f]+) (....) ([0-9a-f]+) '
r'(..):(..) (\d+) *(.*)$')
detail = re.compile(r'^(.*): +(\d+) kB')
for line in maps:
m = header.match(line)
if m:
vma = VMA(*parsed_groups(m, hex, hex, str, hex, str, str, int, str))
ret.append(vma)
else:
m = detail.match(line)
if m:
k, v = parsed_groups(m, str, int)
assert k not in vma
vma[k] = v
else:
print 'unparseable line:', line
return ret
perms = permute(['r-', 'w-', 'x-', 'ps'])
def make_summary_dicts(vmas):
mapped = {}
anon = {}
for d in mapped, anon:
# per-perm
for k in perms:
d[k] = {}
d[k]['Size'] = 0
for y in 'Shared', 'Private':
d[k][y] = {}
for z in 'Clean', 'Dirty':
d[k][y][z] = 0
# totals
for y in 'Shared', 'Private':
d[y] = {}
for z in 'Clean', 'Dirty':
d[y][z] = 0
for vma in vmas:
if vma.major == '00' and vma.minor == '00':
d = anon
else:
d = mapped
for y in 'Shared', 'Private':
for z in 'Clean', 'Dirty':
d[vma.perms][y][z] += vma.get(y + '_' + z, 0)
d[y][z] += vma.get(y + '_' + z, 0)
d[vma.perms]['Size'] += vma.get('Size', 0)
return mapped, anon
def values(d, args):
if args:
ret = ()
first = args[0]
for k in first:
ret += values(d[k], args[1:])
return ret
else:
return (d,)
def print_summary(dicts_and_titles):
def desc(title, perms):
ret = {('Anonymous', 'rw-p'): 'Data (malloc, mmap)',
('Anonymous', 'rwxp'): 'Writable code (stack)',
('Mapped', 'r-xp'): 'Code',
('Mapped', 'rwxp'): 'Writable code (jump tables)',
('Mapped', 'r--p'): 'Read-only data',
('Mapped', 'rw-p'): 'Data'}.get((title, perms), None)
if ret:
return ' -- ' + ret
else:
return ''
for d, title in dicts_and_titles:
print title, 'memory:'
print ' Shared Private'
print ' Clean Dirty Clean Dirty'
for k in perms:
if d[k]['Size']:
print (' %s %7d %7d %7d %7d%s'
% ((k,)
+ values(d[k], (('Shared', 'Private'),
('Clean', 'Dirty')))
+ (desc(title, k),)))
print (' total %7d %7d %7d %7d'
% values(d, (('Shared', 'Private'),
('Clean', 'Dirty'))))
print ' ' + '-' * 40
print (' total %7d %7d %7d %7d'
% tuple(map(sum, zip(*[values(d, (('Shared', 'Private'),
('Clean', 'Dirty')))
for d, title in dicts_and_titles]))))
def print_stats(pid=None):
if pid is None:
pid = os.getpid()
vmas = parse_smaps(pid)
mapped, anon = make_summary_dicts(vmas)
print_summary(((mapped, "Mapped"), (anon, "Anonymous")))
def linux_memory(since=0.0): def linux_memory(since=0.0):
'''Return memory usage in bytes. vmas = parse_smaps(os.getpid())
''' mapped, anon = make_summary_dicts(vmas)
return _VmB('VmSize:') - since dicts_and_titles = ((mapped, "Mapped"), (anon, "Anonymous"))
totals = tuple(map(sum, zip(*[values(d, (('Shared', 'Private'),
('Clean', 'Dirty')))
for d, title in dicts_and_titles])))
return (totals[-1]/1024.) - since
def resident(since=0.0):
'''Return resident memory usage in bytes.
'''
return _VmB('VmRSS:') - since
def stacksize(since=0.0):
'''Return stack size in bytes.
'''
return _VmB('VmStk:') - since
## end of http://code.activestate.com/recipes/286222/ }}}
memory = linux_memory memory = linux_memory
elif iswindows: elif iswindows:
import win32process import win32process
import win32con import win32con
@ -95,7 +191,7 @@ elif iswindows:
def win_memory(since=0.0): def win_memory(since=0.0):
info = meminfo(get_handle(os.getpid())) info = meminfo(get_handle(os.getpid()))
return info['WorkingSetSize'] - since return (info['WorkingSetSize']/1024.**2) - since
memory = win_memory memory = win_memory