#! /usr/bin/env python
##############################################################################
#
# Copyright (c) 2001-2005 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Cache simulation.

Usage: simul.py [-s size] tracefile

Options:
-s size: cache size in MB (default 20 MB)
"""

import sys
import time
import getopt
import struct
import math
import bisect
from sets import Set

from ZODB.utils import z64

def usage(msg):
    print >> sys.stderr, msg
    print >> sys.stderr, __doc__

def main():
    # Parse options.
    MB = 1024**2
    cachelimit = 20*MB
    simclass = CircularCacheSimulation
    try:
        opts, args = getopt.getopt(sys.argv[1:], "bflyz2cOaTUs:")
    except getopt.error, msg:
        usage(msg)
        return 2
    for o, a in opts:
        if o == '-b':
            simclass = BuddyCacheSimulation
        elif o == '-f':
            simclass = SimpleCacheSimulation
        elif o == '-l':
            simclass = LRUCacheSimulation
        elif o == '-y':
            simclass = AltZEOCacheSimulation
        elif o == '-z':
            simclass = ZEOCacheSimulation
        elif o == '-s':
            cachelimit = int(float(a)*MB)
        elif o == '-2':
            simclass = TwoQSimluation
        elif o == '-c':
            simclass = CircularCacheSimulation
        elif o == '-O':
            simclass = OracleSimulation
        elif o == '-a':
            simclass = ARCCacheSimulation
        elif o == '-T':
            simclass = ThorSimulation
        elif o == '-U':
            simclass = UnboundedSimulation
        else:
            assert False, (o, a)

    if len(args) != 1:
        usage("exactly one file argument required")
        return 2
    filename = args[0]

    # Open file.
    if filename.endswith(".gz"):
        # Open gzipped file.
        try:
            import gzip
        except ImportError:
            print >> sys.stderr, "can't read gzipped files (no module gzip)"
            return 1
        try:
            f = gzip.open(filename, "rb")
        except IOError, msg:
            print >> sys.stderr, "can't open %s: %s" % (filename, msg)
            return 1
    elif filename == "-":
        # Read from stdin.
        f = sys.stdin
    else:
        # Open regular file.
        try:
            f = open(filename, "rb")
        except IOError, msg:
            print >> sys.stderr, "can't open %s: %s" % (filename, msg)
            return 1

    # Create simulation object.
    if simclass is OracleSimulation:
        sim = simclass(cachelimit, filename)
    else:
        sim = simclass(cachelimit)

    # Print output header.
    sim.printheader()

    # Read trace file, simulating cache behavior.
    f_read = f.read
    unpack = struct.unpack
    FMT = ">iiH8s8s"
    FMT_SIZE = struct.calcsize(FMT)
    assert FMT_SIZE == 26
    while 1:
        # Read a record and decode it.
        r = f_read(FMT_SIZE)
        if len(r) < FMT_SIZE:
            break
        ts, code, oidlen, start_tid, end_tid = unpack(FMT, r)
        if ts == 0:
            # Must be a misaligned record caused by a crash; skip 8 bytes
            # and try again.  Why 8?  Lost in the mist of history.
            f.seek(f.tell() - FMT_SIZE + 8)
            continue
        oid = f_read(oidlen)
        if len(oid) < oidlen:
            break
        # Decode the code.
        dlen, version, code = (code & 0x7fffff00,
                               code & 0x80,
                               code & 0x7e)
        # And pass it to the simulation.
        sim.event(ts, dlen, version, code, oid, start_tid, end_tid)

    f.close()
    # Finish simulation.
    sim.finish()

    # Exit code from main().
    return 0

class Simulation(object):
    """Base class for simulations.

    The driver program calls: event(), printheader(), finish().

    The standard event() method calls these additional methods:
    write(), load(), inval(), report(), restart(); the standard
    finish() method also calls report().
    """

    def __init__(self, cachelimit):
        self.cachelimit = cachelimit
        # Initialize global statistics.
        self.epoch = None
        self.total_loads = 0
        self.total_hits = 0       # subclass must increment
        self.total_invals = 0     # subclass must increment
        self.total_writes = 0
        if not hasattr(self, "extras"):
            self.extras = (self.extraname,)
        self.format = self.format + " %7s" * len(self.extras)
        # Reset per-run statistics and set up simulation data.
        self.restart()

    def restart(self):
        # Reset per-run statistics.
        self.loads = 0
        self.hits = 0       # subclass must increment
        self.invals = 0     # subclass must increment
        self.writes = 0
        self.ts0 = None

    def event(self, ts, dlen, _version, code, oid,
              start_tid, end_tid):
        # Record first and last timestamp seen.
        if self.ts0 is None:
            self.ts0 = ts
            if self.epoch is None:
                self.epoch = ts
        self.ts1 = ts

        # Simulate cache behavior.  Caution:  the codes in the trace file
        # record whether the actual cache missed or hit on each load, but
        # that bears no necessary relationship to whether the simulated cache
        # will hit or miss.  Relatedly, if the actual cache needed to store
        # an object, the simulated cache may not need to (it may already
        # have the data).
        action = code & 0x70
        if action == 0x20:
            # Load.
            self.loads += 1
            self.total_loads += 1
            # Asserting that dlen is 0 iff it's a load miss.
            # assert (dlen == 0) == (code in (0x20, 0x24))
            self.load(oid, dlen, start_tid)
        elif action == 0x50:
            # Store.
            assert dlen
            self.write(oid, dlen, start_tid, end_tid)
        elif action == 0x10:
            # Invalidate.
            self.inval(oid, start_tid)
        elif action == 0x00:
            # Restart.
            self.report()
            self.restart()
        else:
            raise ValueError("unknown trace code 0x%x" % code)

    def write(self, oid, size, start_tid, end_tid):
        pass

    def load(self, oid, size, start_tid):
        # Must increment .hits and .total_hits as appropriate.
        pass

    def inval(self, oid, start_tid):
        # Must increment .invals and .total_invals as appropriate.
        pass

    format = "%12s %9s %8s %8s %6s %6s %7s"

    # Subclass should override extraname to name known instance variables;
    # if extraname is 'foo', both self.foo and self.total_foo must exist:
    extraname = "*** please override ***"

    def printheader(self):
        print "%s, cache size %s bytes" % (self.__class__.__name__,
                                           addcommas(self.cachelimit))
        self.extraheader()
        extranames = tuple([s.upper() for s in self.extras])
        args = ("START TIME", "DURATION", "LOADS", "HITS",
                "INVALS", "WRITES", "HITRATE") + extranames
        print self.format % args

    def extraheader(self):
        pass

    nreports = 0

    def report(self, extratext=''):
        if self.loads:
            self.nreports += 1
            args = (time.ctime(self.ts0)[4:-8],
                    duration(self.ts1 - self.ts0),
                    self.loads, self.hits, self.invals, self.writes,
                    hitrate(self.loads, self.hits))
            args += tuple([getattr(self, name) for name in self.extras])
            print self.format % args, extratext

    def finish(self):
        # Make sure that the last line of output ends with "OVERALL".  This
        # makes it much easier for another program parsing the output to
        # find summary statistics.
        if self.nreports < 2:
            self.report('OVERALL')
        else:
            self.report()
            args = (
                time.ctime(self.epoch)[4:-8],
                duration(self.ts1 - self.epoch),
                self.total_loads,
                self.total_hits,
                self.total_invals,
                self.total_writes,
                hitrate(self.total_loads, self.total_hits))
            args += tuple([getattr(self, "total_" + name)
                           for name in self.extras])
            print (self.format + " OVERALL") % args


# For use in CircularCacheSimulation.
class CircularCacheEntry(object):
    __slots__ = (# object key:  an (oid, start_tid) pair, where
                 # start_tid is the tid of the transaction that created
                 # this revision of oid
                 'key',

                 # tid of transaction that created the next revision;
                 # z64 iff this is the current revision
                 'end_tid',

                 # Offset from start of file to the object's data
                 # record; this includes all overhead bytes (status
                 # byte, size bytes, etc).
                 'offset',
                )

    def __init__(self, key, end_tid, offset):
        self.key = key
        self.end_tid = end_tid
        self.offset = offset

from ZEO.cache import ZEC3_HEADER_SIZE

class CircularCacheSimulation(Simulation):
    """Simulate the ZEO 3.0 cache."""

    # The cache is managed as a single file with a pointer that
    # goes around the file, circularly, forever.  New objects
    # are written at the current pointer, evicting whatever was
    # there previously.

    extras = "evicts", "inuse"

    def __init__(self, cachelimit):
        from ZEO import cache

        Simulation.__init__(self, cachelimit)
        self.total_evicts = 0  # number of cache evictions

        # Current offset in file.
        self.offset = ZEC3_HEADER_SIZE

        # Map offset in file to (size, CircularCacheEntry) pair, or to
        # (size, None) if the offset starts a free block.
        self.filemap = {ZEC3_HEADER_SIZE: (self.cachelimit - ZEC3_HEADER_SIZE,
                                           None)}
        # Map key to CircularCacheEntry.  A key is an (oid, tid) pair.
        self.key2entry = {}

        # Map oid to tid of current revision.
        self.current = {}

        # Map oid to list of (start_tid, end_tid) pairs in sorted order.
        # Used to find matching key for load of non-current data.
        self.noncurrent = {}

        # The number of overhead bytes needed to store an object pickle
        # on disk (all bytes beyond those needed for the object pickle).
        self.overhead = (cache.Object.TOTAL_FIXED_SIZE +
                         cache.OBJECT_HEADER_SIZE)

    def restart(self):
        Simulation.restart(self)
        self.evicts = 0

    def load(self, oid, size, tid):
        if tid == z64:
            # Trying to load current revision.
            if oid in self.current: # else it's a cache miss
                self.hits += 1
                self.total_hits += 1
            return

        # May or may not be trying to load current revision.
        cur_tid = self.current.get(oid)
        if cur_tid == tid:
            self.hits += 1
            self.total_hits += 1
            return

        # It's a load for non-current data.  Do we know about this oid?
        L = self.noncurrent.get(oid)
        if L is None:
            return  # cache miss
        i = bisect.bisect_left(L, (tid, None))
        if i == 0:
            # This tid is smaller than any we know about -- miss.
            return
        lo, hi = L[i-1]
        assert lo < tid
        if tid > hi:
            # No data in the right tid range -- miss.
            return
        # Cache hit.
        self.hits += 1
        self.total_hits += 1

    # (oid, tid) is in the cache.  Remove it:  take it out of key2entry,
    # and in `filemap` mark the space it occupied as being free.  The
    # caller is responsible for removing it from `current` or `noncurrent`.
    def _remove(self, oid, tid):
        key = oid, tid
        e = self.key2entry.pop(key)
        pos = e.offset
        size, _e = self.filemap[pos]
        assert e is _e
        self.filemap[pos] = size, None

    def _remove_noncurrent_revisions(self, oid):
        noncurrent_list = self.noncurrent.get(oid)
        if noncurrent_list:
            self.invals += len(noncurrent_list)
            self.total_invals += len(noncurrent_list)
            for start_tid, end_tid in noncurrent_list:
                self._remove(oid, start_tid)
            del self.noncurrent[oid]

    def inval(self, oid, tid):
        if tid == z64:
            # This is part of startup cache verification:  forget everything
            # about this oid.
            self._remove_noncurrent_revisions(oid)

        cur_tid = self.current.get(oid)
        if cur_tid is None:
            # We don't have current data, so nothing more to do.
            return

        # We had current data for oid, but no longer.
        self.invals += 1
        self.total_invals += 1
        del self.current[oid]
        if tid == z64:
            # Startup cache verification:  forget this oid entirely.
            self._remove(oid, cur_tid)
            return

        # Our current data becomes non-current data.
        # Add the validity range to the list of non-current data for oid.
        assert cur_tid < tid
        L = self.noncurrent.setdefault(oid, [])
        bisect.insort_left(L, (cur_tid, tid))
        # Update the end of oid's validity range in its CircularCacheEntry.
        e = self.key2entry[oid, cur_tid]
        assert e.end_tid == z64
        e.end_tid = tid

    def write(self, oid, size, start_tid, end_tid):
        if end_tid == z64:
            # Storing current revision.
            if oid in self.current:  # we already have it in cache
                return
            self.current[oid] = start_tid
            self.writes += 1
            self.total_writes += 1
            self.add(oid, size, start_tid)
            return
        # Storing non-current revision.
        L = self.noncurrent.setdefault(oid, [])
        p = start_tid, end_tid
        if p in L:
            return  # we already have it in cache
        bisect.insort_left(L, p)
        self.writes += 1
        self.total_writes += 1
        self.add(oid, size, start_tid, end_tid)

    # Add `oid` to the cache, evicting objects as needed to make room.
    # This updates `filemap` and `key2entry`; it's the caller's
    # responsibilty to update `current` or `noncurrent` appropriately.
    def add(self, oid, size, start_tid, end_tid=z64):
        key = oid, start_tid
        assert key not in self.key2entry
        size += self.overhead
        avail = self.makeroom(size)
        e = CircularCacheEntry(key, end_tid, self.offset)
        self.filemap[self.offset] = size, e
        self.key2entry[key] = e
        self.offset += size
        # All the space made available must be accounted for in filemap.
        excess = avail - size
        if excess:
            self.filemap[self.offset] = excess, None

    # Evict enough objects to make at least `need` contiguous bytes, starting
    # at `self.offset`, available.  Evicted objects are removed from
    # `filemap`, `key2entry`, `current` and `noncurrent`.  The caller is
    # responsible for adding new entries to `filemap` to account for all
    # the freed bytes, and for advancing `self.offset`.  The number of bytes
    # freed is the return value, and will be >= need.
    def makeroom(self, need):
        if self.offset + need > self.cachelimit:
            self.offset = ZEC3_HEADER_SIZE
        pos = self.offset
        while need > 0:
            assert pos < self.cachelimit
            size, e = self.filemap.pop(pos)
            if e:   # there is an object here (else it's already free space)
                self.evicts += 1
                self.total_evicts += 1
                assert pos == e.offset
                _e = self.key2entry.pop(e.key)
                assert e is _e
                oid, start_tid = e.key
                if e.end_tid == z64:
                    del self.current[oid]
                else:
                    L = self.noncurrent[oid]
                    L.remove((start_tid, e.end_tid))
            need -= size
            pos += size
        return pos - self.offset  # total number of bytes freed

    def report(self):
        self.check()
        free = used = total = 0
        for size, e in self.filemap.itervalues():
            total += size
            if e:
                used += size
            else:
                free += size

        self.inuse = round(100.0 * used / total, 1)
        self.total_inuse = self.inuse
        Simulation.report(self)

    def check(self):
        oidcount = 0
        pos = ZEC3_HEADER_SIZE
        while pos < self.cachelimit:
            size, e = self.filemap[pos]
            if e:
                oidcount += 1
                assert self.key2entry[e.key].offset == pos
            pos += size
        assert oidcount == len(self.key2entry)
        assert pos == self.cachelimit

    def dump(self):
        print len(self.filemap)
        L = list(self.filemap)
        L.sort()
        for k in L:
            v = self.filemap[k]
            print k, v[0], repr(v[1])

#############################################################################
# CAUTION:  It's most likely that none of the simulators below this
# point work anymore.  A great many changes were needed to teach
# CircularCacheSimulation (above) about MVCC, including method signature
# changes and changes in cache file format, and none of the other simulator
# classes were changed.
#############################################################################

class ZEOCacheSimulation(Simulation):
    """Simulate the ZEO 1.0 and 2.0 cache behavior.

    This assumes the cache is not persistent (we don't know how to
    simulate cache validation.)
    """

    extraname = "flips"

    def __init__(self, cachelimit):
        # Initialize base class
        Simulation.__init__(self, cachelimit)
        # Initialize additional global statistics
        self.total_flips = 0

    def restart(self):
        # Reset base class
        Simulation.restart(self)
        # Reset additional per-run statistics
        self.flips = 0
        # Set up simulation
        self.filesize = [4, 4] # account for magic number
        self.fileoids = [{}, {}]
        self.current = 0 # index into filesize, fileoids

    def load(self, oid, size):
        if (self.fileoids[self.current].get(oid) or
            self.fileoids[1 - self.current].get(oid)):
            self.hits += 1
            self.total_hits += 1
        else:
            self.write(oid, size)

    def write(self, oid, size):
        # Fudge because size is rounded up to multiples of 256.  (31
        # is header overhead per cache record; 127 is to compensate
        # for rounding up to multiples of 256.)
        size = size + 31 - 127
        if self.filesize[self.current] + size > self.cachelimit / 2:
            # Cache flip
            self.flips += 1
            self.total_flips += 1
            self.current = 1 - self.current
            self.filesize[self.current] = 4
            self.fileoids[self.current] = {}
        self.filesize[self.current] += size
        self.fileoids[self.current][oid] = 1

    def inval(self, oid):
        if self.fileoids[self.current].get(oid):
            self.invals += 1
            self.total_invals += 1
            del self.fileoids[self.current][oid]
        elif self.fileoids[1 - self.current].get(oid):
            self.invals += 1
            self.total_invals += 1
            del self.fileoids[1 - self.current][oid]

class AltZEOCacheSimulation(ZEOCacheSimulation):
    """A variation of the ZEO cache that copies to the current file.

    When a hit is found in the non-current cache file, it is copied to
    the current cache file.  Exception: when the copy would cause a
    cache flip, we don't copy (this is part laziness, part concern
    over causing extraneous flips).
    """

    def load(self, oid, size):
        if self.fileoids[self.current].get(oid):
            self.hits += 1
            self.total_hits += 1
        elif self.fileoids[1 - self.current].get(oid):
            self.hits += 1
            self.total_hits += 1
            # Simulate a write, unless it would cause a flip
            size = size + 31 - 127
            if self.filesize[self.current] + size <= self.cachelimit / 2:
                self.filesize[self.current] += size
                self.fileoids[self.current][oid] = 1
                del self.fileoids[1 - self.current][oid]
        else:
            self.write(oid, size)

class LRUCacheSimulation(Simulation):

    extraname = "evicts"

    def __init__(self, cachelimit):
        # Initialize base class
        Simulation.__init__(self, cachelimit)
        # Initialize additional global statistics
        self.total_evicts = 0

    def restart(self):
        # Reset base class
        Simulation.restart(self)
        # Reset additional per-run statistics
        self.evicts = 0
        # Set up simulation
        self.cache = {}
        self.size = 0
        self.head = Node(None, None)
        self.head.linkbefore(self.head)

    def load(self, oid, size):
        node = self.cache.get(oid)
        if node is not None:
            self.hits += 1
            self.total_hits += 1
            node.linkbefore(self.head)
        else:
            self.write(oid, size)

    def write(self, oid, size):
        node = self.cache.get(oid)
        if node is not None:
            node.unlink()
            assert self.head.next is not None
            self.size -= node.size
        node = Node(oid, size)
        self.cache[oid] = node
        node.linkbefore(self.head)
        self.size += size
        # Evict LRU nodes
        while self.size > self.cachelimit:
            self.evicts += 1
            self.total_evicts += 1
            node = self.head.next
            assert node is not self.head
            node.unlink()
            assert self.head.next is not None
            del self.cache[node.oid]
            self.size -= node.size

    def inval(self, oid):
        node = self.cache.get(oid)
        if node is not None:
            assert node.oid == oid
            self.invals += 1
            self.total_invals += 1
            node.unlink()
            assert self.head.next is not None
            del self.cache[oid]
            self.size -= node.size
            assert self.size >= 0

class Node(object):
    """Node in a doubly-linked list, storing oid and size as payload.

    A node can be linked or unlinked; in the latter case, next and
    prev are None.  Initially a node is unlinked.
    """

    __slots__ = ['prev', 'next', 'oid', 'size']

    def __init__(self, oid, size):
        self.oid = oid
        self.size = size
        self.prev = self.next = None

    def unlink(self):
        prev = self.prev
        next = self.next
        if prev is not None:
            assert next is not None
            assert prev.next is self
            assert next.prev is self
            prev.next = next
            next.prev = prev
            self.prev = self.next = None
        else:
            assert next is None

    def linkbefore(self, next):
        self.unlink()
        prev = next.prev
        if prev is None:
            assert next.next is None
            prev = next
        self.prev = prev
        self.next = next
        prev.next = next.prev = self

am = object()
a1in = object()
a1out = object()

class Node2Q(Node):

    __slots__ = ["kind", "hits"]

    def __init__(self, oid, size, kind=None):
        Node.__init__(self, oid, size)
        self.kind = kind
        self.hits = 0

    def linkbefore(self, next):
        if next.kind != self.kind:
            self.kind = next.kind
        Node.linkbefore(self, next)

class TwoQSimluation(Simulation):
    # The original 2Q algorithm is page based and the authors offer
    # tuning guidlines based on a page-based cache.  Our cache is
    # object based, so, for example, it's hard to compute the number
    # of oids to store in a1out based on the size of a1in.

    extras = "evicts", "hothit", "am_add"

    NodeClass = Node2Q

    def __init__(self, cachelimit, outlen=10000, threshold=0):
        Simulation.__init__(self, cachelimit)

        # The promotion threshold: If a hit occurs in a1out, it is
        # promoted to am if the number of hits on the object while it
        # was in a1in is at least threshold.  The standard 2Q scheme
        # uses a threshold of 0.
        self.threshold = threshold
        self.am_limit = 3 * self.cachelimit / 4
        self.a1in_limit = self.cachelimit / 4

        self.cache = {}
        self.am_size = 0
        self.a1in_size = 0
        self.a1out_size = 0

        self.total_evicts = 0
        self.total_hothit = 0
        self.total_am_add = 0
        self.a1out_limit = outlen

        # An LRU queue of hot objects
        self.am = self.NodeClass(None, None, am)
        self.am.linkbefore(self.am)
        # A FIFO queue of recently referenced objects.  It's purpose
        # is to absorb references to objects that are accessed a few
        # times in short order, then forgotten about.
        self.a1in = self.NodeClass(None, None, a1in)
        self.a1in.linkbefore(self.a1in)
        # A FIFO queue of recently reference oids.
        # This queue only stores the oids, not any data.  If we get a
        # hit in this queue, promote the object to am.
        self.a1out = self.NodeClass(None, None, a1out)
        self.a1out.linkbefore(self.a1out)

    def makespace(self, size):
        for space in 0, size:
            if self.enoughspace(size):
                return
            self.evict_a1in(space)
            if self.enoughspace(size):
                return
            self.evict_am(space)

    def enoughspace(self, size):
        totalsize = self.a1in_size + self.am_size
        return totalsize + size < self.cachelimit

    def evict_a1in(self, extra):
        while self.a1in_size + extra > self.a1in_limit:
            if self.a1in.next is self.a1in:
                return
            assert self.a1in.next is not None
            node = self.a1in.next
            self.evicts += 1
            self.total_evicts += 1
            node.linkbefore(self.a1out)
            self.a1out_size += 1
            self.a1in_size -= node.size
            if self.a1out_size > self.a1out_limit:
                assert self.a1out.next is not None
                node = self.a1out.next
                node.unlink()
                del self.cache[node.oid]
                self.a1out_size -= 1

    def evict_am(self, extra):
        while self.am_size + extra > self.am_limit:
            if self.am.next is self.am:
                return
            assert self.am.next is not None
            node = self.am.next
            self.evicts += 1
            self.total_evicts += 1
            # This node hasn't been accessed in a while, so just
            # forget about it.
            node.unlink()
            del self.cache[node.oid]
            self.am_size -= node.size

    def write(self, oid, size):
        # A write always follows a read (ZODB doesn't allow blind writes).
        # So this write must have followed a recent read of the object.
        # Don't change it's position, but do update the size.

        # XXX For now, don't evict pages if the new version of the object
        # is big enough to require eviction.
        node = self.cache.get(oid)
        if node is None or node.kind is a1out:
            return
        if node.kind is am:
            self.am_size = self.am_size - node.size + size
            node.size = size
        else:
            self.a1in_size = self.a1in_size - node.size + size
            node.size = size

    def load(self, oid, size):
        node = self.cache.get(oid)
        if node is not None:
            if node.kind is am:
                self.hits += 1
                self.total_hits += 1
                self.hothit += 1
                self.total_hothit += 1
                node.hits += 1
                node.linkbefore(self.am)
            elif node.kind is a1in:
                self.hits += 1
                self.total_hits += 1
                node.hits += 1
            elif node.kind is a1out:
                self.a1out_size -= 1
                if node.hits >= self.threshold:
                    self.makespace(node.size)
                    self.am_size += node.size
                    node.linkbefore(self.am)
                    self.cache[oid] = node
                    self.am_add += 1
                    self.total_am_add += 1
                else:
                    node.unlink()
                    self.insert(oid, size)
        else:
            self.insert(oid, size)

    def insert(self, oid, size):
        # New objects enter the cache via a1in.  If they
        # are frequently used over a long enough time, they
        # will be promoted to am -- but only via a1out.
        self.makespace(size)
        node = self.NodeClass(oid, size, a1in)
        node.linkbefore(self.a1in)
        self.cache[oid] = node
        self.a1in_size += node.size

    def inval(self, oid):
        # The original 2Q algorithm didn't have to deal with
        # invalidations.  My own solution: Move it to the head of
        # a1out.
        node = self.cache.get(oid)
        if node is None:
            return
        self.invals += 1
        self.total_invals += 1
        # XXX Should an invalidation to a1out count?
        if node.kind is a1out:
            return
        node.linkbefore(self.a1out)
        if node.kind is am:
            self.am_size -= node.size
        else:
            self.a1in_size -= node.size

    def restart(self):
        Simulation.restart(self)

        self.evicts = 0
        self.hothit = 0
        self.am_add = 0

lruT = object()
lruB = object()
fifoT = object()
fifoB = object()

class ARCCacheSimulation(Simulation):

    # Based on the paper ARC: A Self-Tuning, Low Overhead Replacement
    # Cache by Nimrod Megiddo and Dharmendra S. Modha, USENIX FAST
    # 2003.  The paper describes a block-based cache.  A lot of the
    # details need to be fiddled to work with an object-based cache.
    # For size issues, the key insight ended up being conditions
    # A.1-A.5 rather than the details of the algorithm in Fig. 4.

    extras = "lruThits", "evicts", "p"

    def __init__(self, cachelimit):
        Simulation.__init__(self, cachelimit)
        # There are two pairs of linked lists.  Each pair has a top and
        # bottom half.  The bottom half contains metadata, but not actual
        # objects.

        # LRU list of frequently used objects
        self.lruT = Node2Q(None, None, lruT)
        self.lruT.linkbefore(self.lruT)
        self.lruT_len = 0
        self.lruT_size = 0

        self.lruB = Node2Q(None, None, lruB)
        self.lruB.linkbefore(self.lruB)
        self.lruB_len = 0
        self.lruB_size = 0

        # FIFO list of objects seen once
        self.fifoT = Node2Q(None, None, fifoT)
        self.fifoT.linkbefore(self.fifoT)
        self.fifoT_len = 0
        self.fifoT_size = 0

        self.fifoB = Node2Q(None, None, fifoB)
        self.fifoB.linkbefore(self.fifoB)
        self.fifoB_len = 0
        self.fifoB_size = 0

        # maps oid to node
        self.cache = {}

        # The paper says that p should be adjust be 1 as the minimum:
        # "The compound effect of such small increments and decrements
        # to p s quite profound as we will demonstrated in the next
        # section."  Not really, as far as I can tell.  In my traces
        # with a very small cache, it was taking far too long to adjust
        # towards favoring some FIFO component.  I would guess that the
        # chief difference is that our caches are much bigger than the
        # ones they experimented with.  Their biggest cache had 512K
        # entries, while our smallest cache will have 40 times that many
        # entries.

        self.p = 0
        # XXX multiply computed adjustments to p by walk_factor
        self.walk_factor = 500

        # statistics
        self.total_hits = 0
        self.total_lruThits = 0
        self.total_fifoThits = 0
        self.total_evicts = 0

    def restart(self):
        Simulation.restart(self)
        self.hits = 0
        self.lruThits = 0
        self.fifoThits = 0
        self.evicts = 0

    def write(self, oid, size):
        pass

    def replace(self, lruB=False):
        self.evicts += 1
        self.total_evicts += 1
        if self.fifoT_size > self.p or (lruB and self.fifoT_size == self.p):
            node = self.fifoT.next
            if node is self.fifoT:
                return 0
            assert node is not self.fifoT, self.stats()
            node.linkbefore(self.fifoB)
            self.fifoT_len -= 1
            self.fifoT_size -= node.size
            self.fifoB_len += 1
            self.fifoB_size += node.size
        else:
            node = self.lruT.next
            if node is self.lruT:
                return 0
            assert node is not self.lruT, self.stats()
            node.linkbefore(self.lruB)
            self.lruT_len -= 1
            self.lruT_size -= node.size
            self.lruB_len += 1
            self.lruB_size += node.size
        return node.size

    def stats(self):
        self.totalsize = self.lruT_size + self.fifoT_size
        self.allsize = self.totalsize + self.lruB_size + self.fifoB_size
        print "cachelimit = %s totalsize=%s allsize=%s" % (
            addcommas(self.cachelimit),
            addcommas(self.totalsize),
            addcommas(self.allsize))

        fmt = (
            "p=%(p)d\n"
            "lruT  = %(lruT_len)5d / %(lruT_size)8d / %(lruThits)d\n"
            "fifoT = %(fifoT_len)5d / %(fifoT_size)8d / %(fifoThits)d\n"
            "lruB  = %(lruB_len)5d / %(lruB_size)8d\n"
            "fifoB = %(fifoB_len)5d / %(fifoB_size)8d\n"
            "loads=%(loads)d hits=%(hits)d evicts=%(evicts)d\n"
            )
        print fmt % self.__dict__

    def report(self):
        self.total_p = self.p
        Simulation.report(self)
##        self.stats()

    def load(self, oid, size):
##        maybe(self.stats, p=0.002)
        node = self.cache.get(oid)
        if node is None:
            # cache miss: We're going to insert a new object in fifoT.
            # If fifo is full, we'll need to evict something to make
            # room for it.

            prev = need = size
            while need > 0:
                if size + self.fifoT_size + self.fifoB_size >= self.cachelimit:
                    if need + self.fifoT_size >= self.cachelimit:
                        node = self.fifoB.next
                        assert node is not self.fifoB, self.stats()
                        node.unlink()
                        del self.cache[node.oid]
                        self.fifoB_size -= node.size
                        self.fifoB_len -= 1
                        self.evicts += 1
                        self.total_evicts += 1
                    else:
                        node = self.fifoB.next
                        assert node is not self.fifoB, self.stats()
                        node.unlink()
                        del self.cache[node.oid]
                        self.fifoB_size -= node.size
                        self.fifoB_len -= 1
                        if self.fifoT_size + self.lruT_size > self.cachelimit:
                            need -= self.replace()
                else:
                    incache_size = self.fifoT_size + self.lruT_size + need
                    total_size = (incache_size + self.fifoB_size
                                  + self.lruB_size)
                    if total_size >= self.cachelimit * 2:
                        node = self.lruB.next
                        if node is self.lruB:
                            break
                        assert node is not self.lruB
                        node.unlink()
                        del self.cache[node.oid]
                        self.lruB_size -= node.size
                        self.lruB_len -= 1
                    elif incache_size > self.cachelimit:
                        need -= self.replace()
                    else:
                        break
                if need == prev:
                    # XXX hack, apparently we can't get rid of anything else
                    break
                prev = need

            node = Node2Q(oid, size)
            node.linkbefore(self.fifoT)
            self.fifoT_len += 1
            self.fifoT_size += size
            self.cache[oid] = node
        else:
            # a cache hit, but possibly in a bottom list that doesn't
            # actually hold the object
            if node.kind is lruT:
                node.linkbefore(self.lruT)

                self.hits += 1
                self.total_hits += 1
                self.lruThits += 1
                self.total_lruThits += 1

            elif node.kind is fifoT:
                node.linkbefore(self.lruT)
                self.fifoT_len -= 1
                self.lruT_len += 1
                self.fifoT_size -= node.size
                self.lruT_size += node.size

                self.hits += 1
                self.total_hits += 1
                self.fifoThits += 1
                self.total_fifoThits += 1

            elif node.kind is fifoB:
                node.linkbefore(self.lruT)
                self.fifoB_len -= 1
                self.lruT_len += 1
                self.fifoB_size -= node.size
                self.lruT_size += node.size

                # XXX need a better min than 1?
##                print "adapt+", max(1, self.lruB_size // self.fifoB_size)
                delta = max(1, self.lruB_size / max(1, self.fifoB_size))
                self.p += delta * self.walk_factor
                if self.p > self.cachelimit:
                    self.p = self.cachelimit

                need = node.size
                if self.lruT_size + self.fifoT_size + need > self.cachelimit:
                    while need > 0:
                        r = self.replace()
                        if not r:
                            break
                        need -= r

            elif node.kind is lruB:
                node.linkbefore(self.lruT)
                self.lruB_len -= 1
                self.lruT_len += 1
                self.lruB_size -= node.size
                self.lruT_size += node.size

                # XXX need a better min than 1?
##                print "adapt-", max(1, self.fifoB_size // self.lruB_size)
                delta = max(1, self.fifoB_size / max(1, self.lruB_size))
                self.p -= delta * self.walk_factor
                if self.p < 0:
                    self.p = 0

                need = node.size
                if self.lruT_size + self.fifoT_size + need > self.cachelimit:
                    while need > 0:
                        r = self.replace(lruB=True)
                        if not r:
                            break
                        need -= r

    def inval(self, oid):
        pass

    def extraheader(self):
        pass

class OracleSimulation(LRUCacheSimulation):
    # Not sure how to implement this yet.  This is a cache where I
    # cheat to see how good we could actually do.  The cache
    # replacement problem for multi-size caches is NP-hard, so we're
    # not going to have an optimal solution.

    # At the moment, the oracle is mostly blind.  It knows which
    # objects will be referenced more than once, so that it can
    # ignore objects referenced only once.  In most traces, these
    # objects account for about 20% of references.

    def __init__(self, cachelimit, filename):
        LRUCacheSimulation.__init__(self, cachelimit)
        self.count = {}
        self.scan(filename)

    def load(self, oid, size):
        node = self.cache.get(oid)
        if node is not None:
            self.hits += 1
            self.total_hits += 1
            node.linkbefore(self.head)
        else:
            if oid in self.count:
                self.write(oid, size)

    def scan(self, filename):
        # scan the file in advance to figure out which objects will
        # be referenced more than once.
        f = open(filename, "rb")
        struct_unpack = struct.unpack
        f_read = f.read
        offset = 0
        while 1:
            # Read a record and decode it
            r = f_read(8)
            if len(r) < 8:
                break
            offset += 8
            ts, code = struct_unpack(">ii", r)
            if ts == 0:
                # Must be a misaligned record caused by a crash
                ##print "Skipping 8 bytes at offset", offset-8
                continue
            r = f_read(16)
            if len(r) < 16:
                break
            offset += 16
            oid, serial = struct_unpack(">8s8s", r)
            if code & 0x70 == 0x20:
                # only look at loads
                self.count[oid] = self.count.get(oid, 0) + 1

        all = len(self.count)

        # Now remove everything with count == 1
        once = [oid for oid, count in self.count.iteritems()
                if count == 1]
        for oid in once:
            del self.count[oid]

        print "Scanned file, %d unique oids, %d repeats" % (
            all, len(self.count))

class BuddyCacheSimulation(LRUCacheSimulation):

    def __init__(self, cachelimit):
        LRUCacheSimulation.__init__(self, roundup(cachelimit))

    def restart(self):
        LRUCacheSimulation.restart(self)
        self.allocator = self.allocatorFactory(self.cachelimit)

    def allocatorFactory(self, size):
        return BuddyAllocator(size)

    # LRUCacheSimulation.load() is just fine

    def write(self, oid, size):
        node = self.cache.get(oid)
        if node is not None:
            node.unlink()
            assert self.head.next is not None
            self.size -= node.size
            self.allocator.free(node)
        while 1:
            node = self.allocator.alloc(size)
            if node is not None:
                break
            # Failure to allocate.  Evict something and try again.
            node = self.head.next
            assert node is not self.head
            self.evicts += 1
            self.total_evicts += 1
            node.unlink()
            assert self.head.next is not None
            del self.cache[node.oid]
            self.size -= node.size
            self.allocator.free(node)
        node.oid = oid
        self.cache[oid] = node
        node.linkbefore(self.head)
        self.size += node.size

    def inval(self, oid):
        node = self.cache.get(oid)
        if node is not None:
            assert node.oid == oid
            self.invals += 1
            self.total_invals += 1
            node.unlink()
            assert self.head.next is not None
            del self.cache[oid]
            self.size -= node.size
            assert self.size >= 0
            self.allocator.free(node)

class SimpleCacheSimulation(BuddyCacheSimulation):

    def allocatorFactory(self, size):
        return SimpleAllocator(size)

    def finish(self):
        BuddyCacheSimulation.finish(self)
        self.allocator.report()

MINSIZE = 256

class BuddyAllocator:

    def __init__(self, cachelimit):
        cachelimit = roundup(cachelimit)
        self.cachelimit = cachelimit
        self.avail = {} # Map rounded-up sizes to free list node heads
        self.nodes = {} # Map address to node
        k = MINSIZE
        while k <= cachelimit:
            self.avail[k] = n = Node(None, None) # Not BlockNode; has no addr
            n.linkbefore(n)
            k += k
        node = BlockNode(None, cachelimit, 0)
        self.nodes[0] = node
        node.linkbefore(self.avail[cachelimit])

    def alloc(self, size):
        size = roundup(size)
        k = size
        while k <= self.cachelimit:
            head = self.avail[k]
            node = head.next
            if node is not head:
                break
            k += k
        else:
            return None # Store is full, or block is too large
        node.unlink()
        size2 = node.size
        while size2 > size:
            size2 = size2 / 2
            assert size2 >= size
            node.size = size2
            buddy = BlockNode(None, size2, node.addr + size2)
            self.nodes[buddy.addr] = buddy
            buddy.linkbefore(self.avail[size2])
        node.oid = 1 # Flag as in-use
        return node

    def free(self, node):
        assert node is self.nodes[node.addr]
        assert node.prev is node.next is None
        node.oid = None # Flag as free
        while node.size < self.cachelimit:
            buddy_addr = node.addr ^ node.size
            buddy = self.nodes[buddy_addr]
            assert buddy.addr == buddy_addr
            if buddy.oid is not None or buddy.size != node.size:
                break
            # Merge node with buddy
            buddy.unlink()
            if buddy.addr < node.addr: # buddy prevails
                del self.nodes[node.addr]
                node = buddy
            else: # node prevails
                del self.nodes[buddy.addr]
            node.size *= 2
        assert node is self.nodes[node.addr]
        node.linkbefore(self.avail[node.size])

    def dump(self, msg=""):
        if msg:
            print msg,
        size = MINSIZE
        blocks = bytes = 0
        while size <= self.cachelimit:
            head = self.avail[size]
            node = head.next
            count = 0
            while node is not head:
                count += 1
                node = node.next
            if count:
                print "%d:%d" % (size, count),
            blocks += count
            bytes += count*size
            size += size
        print "-- %d, %d" % (bytes, blocks)

def roundup(size):
    k = MINSIZE
    while k < size:
        k += k
    return k

class SimpleAllocator:

    def __init__(self, arenasize):
        self.arenasize = arenasize
        self.avail = BlockNode(None, 0, 0) # Weird: empty block as list head
        self.rover = self.avail
        node = BlockNode(None, arenasize, 0)
        node.linkbefore(self.avail)
        self.taglo = {0: node}
        self.taghi = {arenasize: node}
        # Allocator statistics
        self.nallocs = 0
        self.nfrees = 0
        self.allocloops = 0
        self.freebytes = arenasize
        self.freeblocks = 1
        self.allocbytes = 0
        self.allocblocks = 0

    def report(self):
        print ("NA=%d AL=%d NF=%d ABy=%d ABl=%d FBy=%d FBl=%d" %
               (self.nallocs, self.allocloops,
                self.nfrees,
                self.allocbytes, self.allocblocks,
                self.freebytes, self.freeblocks))

    def alloc(self, size):
        self.nallocs += 1
        # First fit algorithm
        rover = stop = self.rover
        while 1:
            self.allocloops += 1
            if rover.size >= size:
                break
            rover = rover.next
            if rover is stop:
                return None # We went round the list without finding space
        if rover.size == size:
            self.rover = rover.next
            rover.unlink()
            del self.taglo[rover.addr]
            del self.taghi[rover.addr + size]
            self.freeblocks -= 1
            self.allocblocks += 1
            self.freebytes -= size
            self.allocbytes += size
            return rover
        # Take space from the beginning of the roving pointer
        assert rover.size > size
        node = BlockNode(None, size, rover.addr)
        del self.taglo[rover.addr]
        rover.size -= size
        rover.addr += size
        self.taglo[rover.addr] = rover
        #self.freeblocks += 0 # No change here
        self.allocblocks += 1
        self.freebytes -= size
        self.allocbytes += size
        return node

    def free(self, node):
        self.nfrees += 1
        self.freeblocks += 1
        self.allocblocks -= 1
        self.freebytes += node.size
        self.allocbytes -= node.size
        node.linkbefore(self.avail)
        self.taglo[node.addr] = node
        self.taghi[node.addr + node.size] = node
        x = self.taghi.get(node.addr)
        if x is not None:
            # Merge x into node
            x.unlink()
            self.freeblocks -= 1
            del self.taglo[x.addr]
            del self.taghi[x.addr + x.size]
            del self.taglo[node.addr]
            node.addr = x.addr
            node.size += x.size
            self.taglo[node.addr] = node
        x = self.taglo.get(node.addr + node.size)
        if x is not None:
            # Merge x into node
            x.unlink()
            self.freeblocks -= 1
            del self.taglo[x.addr]
            del self.taghi[x.addr + x.size]
            del self.taghi[node.addr + node.size]
            node.size += x.size
            self.taghi[node.addr + node.size] = node
        # It's possible that either one of the merges above invalidated
        # the rover.
        # It's simplest to simply reset the rover to the newly freed block.
        self.rover = node

    def dump(self, msg=""):
        if msg:
            print msg,
        count = 0
        bytes = 0
        node = self.avail.next
        while node is not self.avail:
            bytes += node.size
            count += 1
            node = node.next
        print count, "free blocks,", bytes, "free bytes"
        self.report()

class BlockNode(Node):

    __slots__ = ['addr']

    def __init__(self, oid, size, addr):
        Node.__init__(self, oid, size)
        self.addr = addr

def testallocator(factory=BuddyAllocator):
    # Run one of Knuth's experiments as a test
    import random
    import heapq # This only runs with Python 2.3, folks :-)
    reportfreq = 100
    cachelimit = 2**17
    cache = factory(cachelimit)
    queue = []
    T = 0
    blocks = 0
    while T < 5000:
        while queue and queue[0][0] <= T:
            time, node = heapq.heappop(queue)
            assert time == T
            ##print "free addr=%d, size=%d" % (node.addr, node.size)
            cache.free(node)
            blocks -= 1
        size = random.randint(100, 2000)
        lifetime = random.randint(1, 100)
        node = cache.alloc(size)
        if node is None:
            print "out of mem"
            cache.dump("T=%4d: %d blocks;" % (T, blocks))
            break
        else:
            ##print "alloc addr=%d, size=%d" % (node.addr, node.size)
            blocks += 1
            heapq.heappush(queue, (T + lifetime, node))
        T = T+1
        if T % reportfreq == 0:
            cache.dump("T=%4d: %d blocks;" % (T, blocks))

def hitrate(loads, hits):
    return "%5.1f%%" % (100.0 * hits / max(1, loads))

def duration(secs):
    mm, ss = divmod(secs, 60)
    hh, mm = divmod(mm, 60)
    if hh:
        return "%d:%02d:%02d" % (hh, mm, ss)
    if mm:
        return "%d:%02d" % (mm, ss)
    return "%d" % ss

def addcommas(n):
    sign, s = '', str(n)
    if s[0] == '-':
        sign, s = '-', s[1:]
    i = len(s) - 3
    while i > 0:
        s = s[:i] + ',' + s[i:]
        i -= 3
    return sign + s

import random

def maybe(f, p=0.5):
    if random.random() < p:
        f()

#############################################################################
# Thor-like eviction scheme.
#
# The cache keeps a list of all objects, and uses a travelling pointer
# to decay the worth of objects over time.

class ThorNode(Node):

    __slots__ = ['worth']

    def __init__(self, oid, size, worth=None):
        Node.__init__(self, oid, size)
        self.worth = worth

class ThorListHead(Node):
    def __init__(self):
        Node.__init__(self, 0, 0)
        self.next = self.prev = self

class ThorSimulation(Simulation):

    extras = "evicts", "trips"

    def __init__(self, cachelimit):
        Simulation.__init__(self, cachelimit)

        # Maximum total of object sizes we keep in cache.
        self.maxsize = cachelimit
        # Current total of object sizes in cache.
        self.currentsize = 0

        # A worth byte maps to a set of all objects with that worth.
        # This is cheap to keep updated, and makes finding low-worth
        # objects for eviction trivial (just march over the worthsets
        # list, in order).
        self.worthsets = [Set() for dummy in range(256)]

        # We keep a circular list of all objects in cache.  currentobj
        # walks around it forever.  Each time _tick() is called, the
        # worth of currentobj is decreased, basically by shifting
        # right 1, and currentobj moves on to the next object.  When
        # an object is first inserted, it enters the list right before
        # currentobj.  When an object is accessed, its worth is
        # increased by or'ing in 0x80.  This scheme comes from the
        # Thor system, and is an inexpensive way to account for both
        # recency and frequency of access:  recency is reflected in
        # the leftmost bit set, and frequency by how many bits are
        # set.
        #
        # Note:  because evictions are interleaved with ticks,
        # unlinking an object is tricky, lest we evict currentobj.  The
        # class _unlink method takes care of this properly.
        self.listhead = ThorListHead()
        self.currentobj = self.listhead

        # Map an object.oid to its ThorNode.
        self.oid2object = {}

        self.total_evicts = self.total_trips = 0

    # Unlink object from the circular list, taking care not to lose
    # track of the current object.  Always call this instead of
    # invoking obj.unlink() directly.
    def _unlink(self, obj):
        assert obj is not self.listhead
        if obj is self.currentobj:
            self.currentobj = obj.next
        obj.unlink()

    # Change obj.worth to newworth, maintaining invariants.
    def _change_worth(self, obj, newworth):
        if obj.worth != newworth:
            self.worthsets[obj.worth].remove(obj)
            obj.worth = newworth
            self.worthsets[newworth].add(obj)

    def add(self, object):
        assert object.oid not in self.oid2object
        self.oid2object[object.oid] = object

        newsize = self.currentsize + object.size
        if newsize > self.maxsize:
            self._evictbytes(newsize - self.maxsize)
        self.currentsize += object.size
        object.linkbefore(self.currentobj)

        if object.worth is None:
            # Give smaller objects higher initial worth.  This favors kicking
            # out unreferenced large objects before kicking out unreferenced
            # small objects.  On real life traces, this is a significant
            # win for the hit rate.
            object.worth = 32 - int(round(math.log(object.size, 2)))
        self.worthsets[object.worth].add(object)

    # Decrease the worth of the current object, and advance the
    # current object.
    def _tick(self):
        c = self.currentobj
        if c is self.listhead:
            c = c.next
            if c is self.listhead:  # list is empty
                return
            self.total_trips += 1
            self.trips += 1
        self._change_worth(c, (c.worth + 1) >> 1)
        self.currentobj = c.next

    def access(self, oid):
        self._tick()
        obj = self.oid2object.get(oid)
        if obj is None:
            return None
        self._change_worth(obj, obj.worth | 0x80)
        return obj

    # Evict objects of least worth first, until at least nbytes bytes
    # have been freed.
    def _evictbytes(self, nbytes):
        for s in self.worthsets:
            while s:
                if nbytes <= 0:
                    return
                obj = s.pop()
                nbytes -= obj.size
                self._evictobj(obj)

    def _evictobj(self, obj):
        self.currentsize -= obj.size
        self.worthsets[obj.worth].discard(obj)
        del self.oid2object[obj.oid]
        self._unlink(obj)

        self.evicts += 1
        self.total_evicts += 1

    def _evict_without_bumping_evict_stats(self, obj):
        self._evictobj(obj)
        self.evicts -= 1
        self.total_evicts -= 1

    # Simulator overrides from here on.

    def restart(self):
        # Reset base class
        Simulation.restart(self)
        # Reset additional per-run statistics
        self.evicts = self.trips = 0

    def write(self, oid, size):
        obj = self.oid2object.get(oid)
        worth = None
        if obj is not None:
            worth = obj.worth
            self._evict_without_bumping_evict_stats(obj)
        self.add(ThorNode(oid, size, worth))

    def load(self, oid, size):
        if self.access(oid) is not None:
            self.hits += 1
            self.total_hits += 1
        else:
            self.write(oid, size)

    def inval(self, oid):
        obj = self.oid2object.get(oid)
        if obj is not None:
            self.invals += 1
            self.total_invals += 1
            self._evict_without_bumping_evict_stats(obj)

    # Take the "x" off to see additional stats after each restart period.
    def xreport(self):
        Simulation.report(self)
        print 'non-empty worth sets', sum(map(bool, self.worthsets)),
        print 'objects', len(self.oid2object),
        print 'size', self.currentsize

#############################################################################
# Perfection:  What if the cache were unbounded, and never forgot anything?
# This simulator answers that question directly; the cache size parameter
# isn't used.

class UnboundedSimulation(Simulation):

    extraname = 'evicts'   # for some reason we *have* to define >= 1 extra

    def __init__(self, cachelimit):
        Simulation.__init__(self, cachelimit)
        self.oids = Set()
        self.evicts = self.total_evicts = 0

    def write(self, oid, size):
        self.oids.add(oid)

    def load(self, oid, size):
        if oid in self.oids:
            self.hits += 1
            self.total_hits += 1
        else:
            self.oids.add(oid)

    def inval(self, oid):
        if oid in self.oids:
            self.invals += 1
            self.total_invals += 1
            self.oids.remove(oid)

if __name__ == "__main__":
    sys.exit(main())
