#!/usr/bin/python # Copyright 2011 Lars Wirzenius # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import cliapp import ConfigParser import logging import os import sys class Table(object): '''Represent tabular data for formatting purposes.''' sep = ' ' def __init__(self): self.caption = None self.columns = [] self.rows = [] def add_column(self, heading1, heading2, format, left): self.columns.append((heading1, heading2, format, left)) def add_row(self, data): assert len(data) == len(self.columns) self.rows.append(data) def write_plaintext(self, f): if self.caption: f.write('%s\n%s\n\n' % (self.caption, '-' * len(self.caption))) cells = [] cells.append([h1 for h1, h2, format, left in self.columns]) cells.append([h2 for h1, h2, format, left in self.columns]) for row in self.rows: cells.append([self.format_cell(row[i], self.columns[i][2]) for i in range(len(self.columns))]) widths = self.compute_column_widths(cells) f.write(' %s\n' % self.format_headings(widths, 0)) f.write(' %s\n' % self.format_headings(widths, 1)) f.write('\n') for row in self.rows: f.write(' %s\n' % self.format_row(row, widths)) def align_cell(self, width, text, column): h1, h2, fmt, left = self.columns[column] if left: return '%-*s' % (width, text) else: return'%*s' % (width, text) def format_cell(self, data, format): return format % data def compute_column_widths(self, cells): widths = [0] * len(self.columns) for row in cells: for i, data in enumerate(row): widths[i] = max(widths[i], len(data)) return widths def format_headings(self, widths, which): headings = [self.align_cell(widths[i], self.columns[i][which], i) for i in range(len(widths))] return self.sep.join(headings) def format_row(self, row, widths): cells = [self.align_cell(widths[i], self.columns[i][2] % row[i], i) for i in range(len(widths))] return self.sep.join(cells) class SeivotsSummary(cliapp.Application): def process_args(self, args): seivots = [] for filename in args: try: seivots.append(self.read_seivot(filename)) except ConfigParser.NoSectionError: sys.stderr.write('Ignoring %s: not a proper seivot file\n' % filename) for group, caption in self.find_groups(seivots): title = 'Profile: %s' % caption self.output.write('%s\n%s\n\n' % (title, '-' * len(title))) ops = ['backup', 'restore', 'list_files', 'forget', 'verify', 'fsck'] for op in ops: self.output.write('Operation: %s\n\n' % op) table = self.make_table(group, op) table.write_plaintext(self.output) self.output.write('\n') def make_table(self, group, op): cols = { 'obnam-revno': ('obnam', 'revno', '%s', self.get_obnam_revision), 'larch-revno': ('larch', 'revno', '%s', self.get_larch_revision), 'gen0-speed': ('gen0', 'Mbit/s', '%.1f', self.get_gen0_speed), 'gen0-time': ('gen0', 's', '%.1f', self.get_gen0_time), 'gen0-ram': ('gen0', 'MiB', '%.1f', self.get_gen0_ram), 'slowest-speed': ('slowest', 'Mbit/s', '%.1f', self.get_slowest_speed), 'slowest-time': ('slowest', 's', '%.1f', self.get_slowest_time), 'biggest-ram': ('biggest', 'MiB', '%.1f', self.get_biggest_ram), 'repo-size': ('repo size', 'MiB', '%.1f', self.get_repo_size), 'repo-writes': ('r-writes', 'MiB', '%.1f', self.get_repo_writes), 'repo-reads': ('r-reads', 'MiB', '%.1f', self.get_repo_reads), 'branch': ('branch', '', '%s', self.get_branch), 'description': ('desc', '', '%s', self.get_description), } leftist = ('branch', 'description') prefix = ('obnam-revno', 'larch-revno') which = { 'backup': ('gen0-time', 'gen0-speed', 'gen0-ram', 'slowest-time', 'slowest-speed', 'biggest-ram', 'repo-size', 'repo-writes', 'repo-reads'), 'restore': ('gen0-time', 'gen0-speed', 'gen0-ram', 'slowest-time', 'slowest-speed', 'biggest-ram','repo-writes', 'repo-reads'), 'list_files': ('gen0-time', 'gen0-ram', 'slowest-time', 'biggest-ram', 'repo-writes', 'repo-reads'), 'forget': ('gen0-time', 'gen0-ram', 'slowest-time', 'biggest-ram', 'repo-writes', 'repo-reads'), 'fsck': ('gen0-time', 'gen0-ram', 'slowest-time', 'biggest-ram', 'repo-writes', 'repo-reads'), 'verify': ('gen0-time', 'gen0-speed', 'gen0-ram', 'slowest-time', 'slowest-speed', 'biggest-ram','repo-writes', 'repo-reads'), } suffix = ('branch', 'description',) colnames = prefix + which[op] + suffix table = Table() for colname in colnames: h1, h2, fmt, func = cols[colname] table.add_column(h1, h2, fmt, colname in leftist) for seivot in group: row = [] for colname in colnames: logging.debug('colname: %s' % colname) h1, h2, fmt, func = cols[colname] value = func(seivot, op) logging.debug('value: %s' % repr(value)) row.append(value) table.add_row(row) return table def read_seivot(self, filename): cp = ConfigParser.ConfigParser() cp.read([filename]) cp.set('meta', 'branch', os.path.basename(os.path.dirname(filename))) return cp def find_groups(self, seivots): # We group together seivot files that share the following: # - initial size # - incremental size # - profile name # - encryption used def key(s): init = self.getint(s, '0', 'backup.new-data') inc = self.getint(s, '1', 'backup.new-data') prof = self.get(s, 'meta', 'profile-name', default='unknown') enc = self.get(s, 'meta', 'encrypted', default='no') return init, inc, prof, enc def caption(s): init, inc, prof, enc = key(s) return ('%s %.0f/%.0f MiB (%s)' % (prof, self.disksize(init), self.disksize(inc), 'encrypted' if enc == 'yes' else 'unencrypted')) groups = {} for seivot in seivots: k = key(seivot) groups[k] = groups.get(k, []) + [seivot] groups = sorted(groups.values(), key=lambda g: key(g[0])) for group in groups: group.sort(key=self.getkey) return [(group, caption(group[0])) for group in groups] def get(self, seivot, group, key, default=None): if seivot.has_option(group, key): return seivot.get(group, key) else: return default def getint(self, seivot, group, key): return int(self.get(seivot, group, key, default=0)) def getfloat(self, seivot, group, key): return float(self.get(seivot, group, key, default=0.0)) def getkey(self, seivot): return (self.get(seivot, 'meta', 'revision'), self.getint(seivot, 'meta', 'larch-revision')) def get_size(self, seivot): return self.getint(seivot, '0', 'backup.new-data') def find_initial_sizes(self, seivots): return list(set(self.get_size(s) for s in seivots)) def find_seivots_in_size_group(self, seivots, size): return [s for s in seivots if self.get_size(s) == size] def values(self, op, suffix, seivot): for section in seivot.sections(): if section not in ['meta', '0']: yield (section, self.getfloat(seivot, section, '%s.%s' % (op,suffix))) def find_slowest_incremental(self, op, seivot): v = list(self.values(op, 'real', seivot)) if not v: return '0' return min(v, key=lambda pair: pair[1])[0] def get_obnam_revision(self, seivot, op): return self.get(seivot, 'meta', 'revision', default='unknown') def get_larch_revision(self, seivot, op): return self.get(seivot, 'meta', 'larch-revision', default='unknown') def get_branch(self, seivot, op): return self.get(seivot, 'meta', 'branch', default='unknown') def get_description(self, seivot, op): return self.get(seivot, 'meta', 'description', default='') def get_gen0_speed(self, seivot, op): bytes = self.getfloat(seivot, '0', 'backup.new-data') secs = self.get_gen0_time(seivot, op) return self.xferspeed(bytes, secs) def get_gen0_time(self, seivot, op): return self.getfloat(seivot, '0', '%s.real' % op) def get_gen0_ram(self, seivot, op): return self.ramsize(self.getfloat(seivot, '0', '%s.maxrss' % op) * 1024) def get_slowest_speed(self, seivot, op): gen = self.find_slowest_incremental(op, seivot) bytes = self.getfloat(seivot, gen, 'backup.new-data') secs = self.getfloat(seivot, gen, '%s.real' % op) return self.xferspeed(bytes, secs) def get_slowest_time(self, seivot, op): gen = self.find_slowest_incremental(op, seivot) secs = self.getfloat(seivot, gen, '%s.real' % op) return secs def get_biggest_ram(self, seivot, op): pair = min(self.values(op, 'maxrss', seivot), key=lambda pair: pair[1]) kilobytes = float(pair[1]) return self.ramsize(kilobytes * 1024) def get_repo_size(self, seivot, op): return self.disksize(self.getfloat(seivot, '0', 'backup.new-data')) def get_repo_writes(self, seivot, op): bytes = self.getfloat(seivot, '0', '%s.repo-bytes-written' % op) return self.disksize(bytes) def get_repo_reads(self, seivot, op): bytes = self.getfloat(seivot, '0', '%s.repo-bytes-read' % op) return self.disksize(bytes) def xferspeed(self, bytes, seconds): if seconds > 0: return 8.0 * bytes / seconds / (1000**2) else: return 0 def ramsize(self, bytes): return float(bytes) / (1024**2) def disksize(self, bytes): return float(bytes) / (1024**2) if __name__ == '__main__': SeivotsSummary().run()