Index: third_party/buildbot_8_4p1/README.chromium |
=================================================================== |
--- third_party/buildbot_8_4p1/README.chromium (revision 127129) |
+++ third_party/buildbot_8_4p1/README.chromium (working copy) |
@@ -1199,3 +1199,289 @@ |
kwargs['logfiles'] = self.logfiles |
# check for the usePTY flag |
+ |
+ |
+Replace the simple (and very slow) LRU cache implementation in BuilderStatus |
+with a better one. Added SyncLRUCache to avoid the expensive concurrency |
+protection in AsyncLRUCache. |
+ |
+Index: buildbot/status/builder.py |
+=================================================================== |
+--- buildbot/status/builder.py (revision 127129) |
++++ buildbot/status/builder.py (working copy) |
+@@ -86,8 +86,8 @@ |
+ self.currentBuilds = [] |
+ self.nextBuild = None |
+ self.watchers = [] |
+- self.buildCache = weakref.WeakValueDictionary() |
+- self.buildCache_LRU = [] |
++ self.buildCache = util.lru.SyncLRUCache(self.cacheMiss, |
++ self.buildCacheSize) |
+ self.logCompressionLimit = False # default to no compression for tests |
+ self.logCompressionMethod = "bz2" |
+ self.logMaxSize = None # No default limit |
+@@ -103,7 +103,6 @@ |
+ d = styles.Versioned.__getstate__(self) |
+ d['watchers'] = [] |
+ del d['buildCache'] |
+- del d['buildCache_LRU'] |
+ for b in self.currentBuilds: |
+ b.saveYourself() |
+ # TODO: push a 'hey, build was interrupted' event |
+@@ -119,8 +118,8 @@ |
+ # when loading, re-initialize the transient stuff. Remember that |
+ # upgradeToVersion1 and such will be called after this finishes. |
+ styles.Versioned.__setstate__(self, d) |
+- self.buildCache = weakref.WeakValueDictionary() |
+- self.buildCache_LRU = [] |
++ self.buildCache = util.lru.SyncLRUCache(self.cacheMiss, |
++ self.buildCacheSize) |
+ self.currentBuilds = [] |
+ self.watchers = [] |
+ self.slavenames = [] |
+@@ -132,6 +131,7 @@ |
+ # gets pickled and unpickled. |
+ if buildmaster.buildCacheSize is not None: |
+ self.buildCacheSize = buildmaster.buildCacheSize |
++ self.buildCache.set_max_size(buildmaster.buildCacheSize) |
+ |
+ def upgradeToVersion1(self): |
+ if hasattr(self, 'slavename'): |
+@@ -186,33 +186,17 @@ |
+ except: |
+ log.msg("unable to save builder %s" % self.name) |
+ log.err() |
+- |
+ |
++ |
+ # build cache management |
+ |
+ def makeBuildFilename(self, number): |
+ return os.path.join(self.basedir, "%d" % number) |
+ |
+- def touchBuildCache(self, build): |
+- self.buildCache[build.number] = build |
+- if build in self.buildCache_LRU: |
+- self.buildCache_LRU.remove(build) |
+- self.buildCache_LRU = self.buildCache_LRU[-(self.buildCacheSize-1):] + [ build ] |
+- return build |
+- |
+ def getBuildByNumber(self, number): |
+- # first look in currentBuilds |
+- for b in self.currentBuilds: |
+- if b.number == number: |
+- return self.touchBuildCache(b) |
++ return self.buildCache.get(number) |
+ |
+- # then in the buildCache |
+- if number in self.buildCache: |
+- metrics.MetricCountEvent.log("buildCache.hits", 1) |
+- return self.touchBuildCache(self.buildCache[number]) |
+- metrics.MetricCountEvent.log("buildCache.misses", 1) |
+- |
+- # then fall back to loading it from disk |
++ def loadBuildFromFile(self, number): |
+ filename = self.makeBuildFilename(number) |
+ try: |
+ log.msg("Loading builder %s's build %d from on-disk pickle" |
+@@ -235,12 +219,20 @@ |
+ build.upgradeLogfiles() |
+ # check that logfiles exist |
+ build.checkLogfiles() |
+- return self.touchBuildCache(build) |
++ return build |
+ except IOError: |
+ raise IndexError("no such build %d" % number) |
+ except EOFError: |
+ raise IndexError("corrupted build pickle %d" % number) |
+ |
++ def cacheMiss(self, number): |
++ # first look in currentBuilds |
++ for b in self.currentBuilds: |
++ if b.number == number: |
++ return b |
++ # then fall back to loading it from disk |
++ return self.loadBuildFromFile(number) |
++ |
+ def prune(self, events_only=False): |
+ # begin by pruning our own events |
+ self.events = self.events[-self.eventHorizon:] |
+@@ -287,7 +279,7 @@ |
+ is_logfile = True |
+ |
+ if num is None: continue |
+- if num in self.buildCache: continue |
++ if num in self.buildCache.cache: continue |
+ |
+ if (is_logfile and num < earliest_log) or num < earliest_build: |
+ pathname = os.path.join(self.basedir, filename) |
+@@ -510,7 +502,7 @@ |
+ assert s.number == self.nextBuildNumber - 1 |
+ assert s not in self.currentBuilds |
+ self.currentBuilds.append(s) |
+- self.touchBuildCache(s) |
++ self.buildCache.put(s.number, s) |
+ |
+ # now that the BuildStatus is prepared to answer queries, we can |
+ # announce the new build to all our watchers |
+@@ -620,7 +612,7 @@ |
+ # Collect build numbers. |
+ # Important: Only grab the *cached* builds numbers to reduce I/O. |
+ current_builds = [b.getNumber() for b in self.currentBuilds] |
+- cached_builds = list(set(self.buildCache.keys() + current_builds)) |
++ cached_builds = list(set(self.buildCache.cache.keys() + current_builds)) |
+ cached_builds.sort() |
+ result['cachedBuilds'] = cached_builds |
+ result['currentBuilds'] = current_builds |
+Index: buildbot/util/lru.py |
+=================================================================== |
+--- buildbot/util/lru.py (revision 127129) |
++++ buildbot/util/lru.py (working copy) |
+@@ -244,5 +244,82 @@ |
+ log.msg(" got:", sorted(self.refcount.items())) |
+ inv_failed = True |
+ |
++ |
++class SyncLRUCache(AsyncLRUCache): |
++ """ |
++ |
++ A least-recently-used cache using the same strategy as AsyncLRUCache, |
++ minus the protections for concurrent access. The motivation for this |
++ class is to provide a speedier implementation for heavily-used caches |
++ that don't need the concurrency protections. |
++ |
++ The constructor takes the same arguments as the AsyncLRUCache |
++ constructor, except C{miss_fn} must return the missing value, I{not} a |
++ deferred. |
++ """ |
++ |
++ # utility function to record recent use of this key |
++ def _ref_key(key): |
++ refcount = self.refcount |
++ queue = self.queue |
++ |
++ queue.append(key) |
++ refcount[key] = refcount[key] + 1 |
++ |
++ # periodically compact the queue by eliminating duplicate keys |
++ # while preserving order of most recent access. Note that this |
++ # is only required when the cache does not exceed its maximum |
++ # size |
++ if len(queue) > self.max_queue: |
++ refcount.clear() |
++ queue_appendleft = queue.appendleft |
++ queue_appendleft(self.sentinel) |
++ for k in ifilterfalse(refcount.__contains__, |
++ iter(queue.pop, self.sentinel)): |
++ queue_appendleft(k) |
++ refcount[k] = 1 |
++ |
++ def get(self, key, **miss_fn_kwargs): |
++ """ |
++ Fetch a value from the cache by key, invoking C{self.miss_fn(key)} if |
++ the key is not in the cache. |
++ |
++ No protection is provided against concurrent access. |
++ |
++ @param key: cache key |
++ @param **miss_fn_kwargs: keyword arguments to the miss_fn |
++ @returns: cache value |
++ """ |
++ cache = self.cache |
++ weakrefs = self.weakrefs |
++ |
++ try: |
++ result = cache[key] |
++ self.hits += 1 |
++ self._ref_key(key) |
++ return result |
++ except KeyError: |
++ try: |
++ result = weakrefs[key] |
++ self.refhits += 1 |
++ cache[key] = result |
++ self._ref_key(key) |
++ return result |
++ except KeyError: |
++ pass |
++ |
++ # if we're here, we've missed and need to fetch |
++ self.misses += 1 |
++ |
++ result = self.miss_fn(key, **miss_fn_kwargs) |
++ if result is not None: |
++ cache[key] = result |
++ weakrefs[key] = result |
++ self._ref_key(key) |
++ self._purge() |
++ |
++ return result |
++ |
++ |
+ # for tests |
+ inv_failed = False |
+Index: buildbot/test/unit/test_status_builder_cache.py |
+=================================================================== |
+--- buildbot/test/unit/test_status_builder_cache.py (revision 0) |
++++ buildbot/test/unit/test_status_builder_cache.py (revision 0) |
+@@ -0,0 +1,60 @@ |
++# This file is part of Buildbot. Buildbot 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, version 2. |
++# |
++# 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, write to the Free Software Foundation, Inc., 51 |
++# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
++# |
++# Copyright Buildbot Team Members |
++ |
++import os |
++from mock import Mock |
++from twisted.trial import unittest |
++from buildbot.status import builder, master |
++ |
++class TestBuildStatus(unittest.TestCase): |
++ |
++ # that buildstep.BuildStepStatus is never instantiated here should tell you |
++ # that these classes are not well isolated! |
++ |
++ def setupBuilder(self, buildername, category=None): |
++ b = builder.BuilderStatus(buildername=buildername, category=category) |
++ # Ackwardly, Status sets this member variable. |
++ b.basedir = os.path.abspath(self.mktemp()) |
++ os.mkdir(b.basedir) |
++ # Otherwise, builder.nextBuildNumber is not defined. |
++ b.determineNextBuildNumber() |
++ # Must initialize these fields before pickling. |
++ b.currentBigState = 'idle' |
++ b.status = 'idle' |
++ return b |
++ |
++ def setupStatus(self, b): |
++ m = Mock() |
++ m.buildbotURL = 'http://buildbot:8010/' |
++ m.basedir = '/basedir' |
++ s = master.Status(m) |
++ b.status = s |
++ return s |
++ |
++ def testBuildCache(self): |
++ b = self.setupBuilder('builder_1') |
++ builds = [] |
++ for i in xrange(5): |
++ build = b.newBuild() |
++ build.setProperty('propkey', 'propval%d' % i, 'test') |
++ builds.append(build) |
++ build.buildStarted(build) |
++ build.buildFinished() |
++ for build in builds: |
++ build2 = b.getBuild(build.number) |
++ self.assertTrue(build2) |
++ self.assertEqual(build2.number, build.number) |
++ self.assertEqual(build.getProperty('propkey'), |
++ 'propval%d' % build.number) |