#! /usr/bin/env python3.3 """ChangelistCache.""" import logging import sys LOG = logging.getLogger('p4gf_copy_to_git').getChild('changelist_cache') class ChangelistCache: """A cache of p4 changes results. Each item is either a P4Changelist or a string containing the path of a P4Changelist. In the case of complete P4Changelist objects, files are not included since: 1) that takes too much space; and 2) they depend on the branch. The cache size is limited to MAX_SIZE. When space is short, P4Changelist items will first be converted to paths, and eventually paths will be dropped. """ MAX_SIZE = 1000000 def __init__(self, p2g): self.p2g = p2g self.changes = {} self.paths = {} self.changenums = set() self.sizeof_changes = 0 self.sizeof_paths = 0 self.hits = 0 self.misses = 0 self.sizeof_discarded = 0 def __del__(self): if self.hits or self.misses: LOG.debug("ChangelistCache hit rate: {} ({}/{}), discarded: {}" .format(self.hits * 100 / (self.hits + self.misses), self.hits, self.hits + self.misses, self.sizeof_discarded)) def get(self, changenum): """Return P4Changelist for changenum.""" cl = self.changes.get(changenum) if cl: self.hits += 1 return cl self.misses += 1 cl = self.p2g._get_changelist(changenum) # pylint: disable=protected-access self._insert(cl) return cl def get_path(self, changenum): """Return only the path of the P4Changelist for changenum.""" cl = self.changes.get(changenum) if cl: self.hits += 1 return self._nonempty_path(cl.path) path = self.paths.get(changenum) if path is not None: self.hits += 1 return self._nonempty_path(path) self.misses += 1 cl = self.p2g._get_changelist(changenum) # pylint: disable=protected-access self._insert(cl) return self._nonempty_path(cl.path) def update(self, cl): """If cl is already cached, update it. Otherwise, insert it. If cache is near capacity, this may result in downgrading a cached P4Changelist to just a path. """ if cl.change in self.changes: del self.changes[cl.change] elif cl.change in self.paths: del self.paths[cl.change] self._insert(cl) def keys(self): """Return list of change numbers we've seen. It's possible a change number will be returned for which no other info is retained in the cache. """ return self.changenums @staticmethod def _nonempty_path(path): """Make sure path isn't empty.""" if path: return path return '//...' def _insert(self, cl): """Add the changelist to the collection.""" # Figure out if we should cache the whole object or just the path. # Once we start caching just paths, never cache complete objects again. if self.sizeof_paths: save_path = True else: sizeof = self._sizeof_change(cl) if sizeof + self.sizeof_changes + self.sizeof_paths > self.MAX_SIZE: save_path = True else: save_path = False if save_path: sizeof = sys.getsizeof(cl.path) # trim until it fits or there's nothing left to trim while ((self.sizeof_changes + self.sizeof_paths) and (sizeof + self.sizeof_changes + self.sizeof_paths > self.MAX_SIZE)): if self.sizeof_changes: chosen = self.changes.popitem() LOG.debug3("changelist-cache dropping change {}".format(chosen.change)) sizeof_chosen = self._sizeof_change(chosen) self.sizeof_changes -= sizeof_chosen self.sizeof_discarded += sizeof_chosen elif self.sizeof_paths: chosen = self.paths.popitem() LOG.debug3("changelist-cache dropping path {}".format(chosen.change)) self.sizeof_paths -= sys.getsizeof(chosen) self.sizeof_discarded += sys.getsizeof(chosen) if save_path: LOG.debug3("changelist-cache adding path {}".format(cl.change)) self.paths[cl.change] = cl.path else: LOG.debug3("changelist-cache adding change {}".format(cl.change)) self.changes[cl.change] = cl self.changenums.add(cl.change) @staticmethod def _sizeof_change(cl): """Calculate the size of a P4Changelist.""" sizeof = (sys.getsizeof(cl.change) + sys.getsizeof(cl.description) + sys.getsizeof(cl.user) + sys.getsizeof(cl.time) + sys.getsizeof(cl.path)) return sizeof