Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/buildbot/buildbot/status/progress.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildbot/buildbot/status/progress.py')
-rw-r--r--buildbot/buildbot/status/progress.py308
1 files changed, 308 insertions, 0 deletions
diff --git a/buildbot/buildbot/status/progress.py b/buildbot/buildbot/status/progress.py
new file mode 100644
index 0000000..dc4d3d5
--- /dev/null
+++ b/buildbot/buildbot/status/progress.py
@@ -0,0 +1,308 @@
+# -*- test-case-name: buildbot.test.test_status -*-
+
+from twisted.internet import reactor
+from twisted.spread import pb
+from twisted.python import log
+from buildbot import util
+
+class StepProgress:
+ """I keep track of how much progress a single BuildStep has made.
+
+ Progress is measured along various axes. Time consumed is one that is
+ available for all steps. Amount of command output is another, and may be
+ better quantified by scanning the output for markers to derive number of
+ files compiled, directories walked, tests run, etc.
+
+ I am created when the build begins, and given to a BuildProgress object
+ so it can track the overall progress of the whole build.
+
+ """
+
+ startTime = None
+ stopTime = None
+ expectedTime = None
+ buildProgress = None
+ debug = False
+
+ def __init__(self, name, metricNames):
+ self.name = name
+ self.progress = {}
+ self.expectations = {}
+ for m in metricNames:
+ self.progress[m] = None
+ self.expectations[m] = None
+
+ def setBuildProgress(self, bp):
+ self.buildProgress = bp
+
+ def setExpectations(self, metrics):
+ """The step can call this to explicitly set a target value for one
+ of its metrics. E.g., ShellCommands knows how many commands it will
+ execute, so it could set the 'commands' expectation."""
+ for metric, value in metrics.items():
+ self.expectations[metric] = value
+ self.buildProgress.newExpectations()
+
+ def setExpectedTime(self, seconds):
+ self.expectedTime = seconds
+ self.buildProgress.newExpectations()
+
+ def start(self):
+ if self.debug: print "StepProgress.start[%s]" % self.name
+ self.startTime = util.now()
+
+ def setProgress(self, metric, value):
+ """The step calls this as progress is made along various axes."""
+ if self.debug:
+ print "setProgress[%s][%s] = %s" % (self.name, metric, value)
+ self.progress[metric] = value
+ if self.debug:
+ r = self.remaining()
+ print " step remaining:", r
+ self.buildProgress.newProgress()
+
+ def finish(self):
+ """This stops the 'time' metric and marks the step as finished
+ overall. It should be called after the last .setProgress has been
+ done for each axis."""
+ if self.debug: print "StepProgress.finish[%s]" % self.name
+ self.stopTime = util.now()
+ self.buildProgress.stepFinished(self.name)
+
+ def totalTime(self):
+ if self.startTime != None and self.stopTime != None:
+ return self.stopTime - self.startTime
+
+ def remaining(self):
+ if self.startTime == None:
+ return self.expectedTime
+ if self.stopTime != None:
+ return 0 # already finished
+ # TODO: replace this with cleverness that graphs each metric vs.
+ # time, then finds the inverse function. Will probably need to save
+ # a timestamp with each setProgress update, when finished, go back
+ # and find the 2% transition points, then save those 50 values in a
+ # list. On the next build, do linear interpolation between the two
+ # closest samples to come up with a percentage represented by that
+ # metric.
+
+ # TODO: If no other metrics are available, just go with elapsed
+ # time. Given the non-time-uniformity of text output from most
+ # steps, this would probably be better than the text-percentage
+ # scheme currently implemented.
+
+ percentages = []
+ for metric, value in self.progress.items():
+ expectation = self.expectations[metric]
+ if value != None and expectation != None:
+ p = 1.0 * value / expectation
+ percentages.append(p)
+ if percentages:
+ avg = reduce(lambda x,y: x+y, percentages) / len(percentages)
+ if avg > 1.0:
+ # overdue
+ avg = 1.0
+ if avg < 0.0:
+ avg = 0.0
+ if percentages and self.expectedTime != None:
+ return self.expectedTime - (avg * self.expectedTime)
+ if self.expectedTime is not None:
+ # fall back to pure time
+ return self.expectedTime - (util.now() - self.startTime)
+ return None # no idea
+
+
+class WatcherState:
+ def __init__(self, interval):
+ self.interval = interval
+ self.timer = None
+ self.needUpdate = 0
+
+class BuildProgress(pb.Referenceable):
+ """I keep track of overall build progress. I hold a list of StepProgress
+ objects.
+ """
+
+ def __init__(self, stepProgresses):
+ self.steps = {}
+ for s in stepProgresses:
+ self.steps[s.name] = s
+ s.setBuildProgress(self)
+ self.finishedSteps = []
+ self.watchers = {}
+ self.debug = 0
+
+ def setExpectationsFrom(self, exp):
+ """Set our expectations from the builder's Expectations object."""
+ for name, metrics in exp.steps.items():
+ s = self.steps[name]
+ s.setExpectedTime(exp.times[name])
+ s.setExpectations(exp.steps[name])
+
+ def newExpectations(self):
+ """Call this when one of the steps has changed its expectations.
+ This should trigger us to update our ETA value and notify any
+ subscribers."""
+ pass # subscribers are not implemented: they just poll
+
+ def stepFinished(self, stepname):
+ assert(stepname not in self.finishedSteps)
+ self.finishedSteps.append(stepname)
+ if len(self.finishedSteps) == len(self.steps.keys()):
+ self.sendLastUpdates()
+
+ def newProgress(self):
+ r = self.remaining()
+ if self.debug:
+ print " remaining:", r
+ if r != None:
+ self.sendAllUpdates()
+
+ def remaining(self):
+ # sum eta of all steps
+ sum = 0
+ for name, step in self.steps.items():
+ rem = step.remaining()
+ if rem == None:
+ return None # not sure
+ sum += rem
+ return sum
+ def eta(self):
+ left = self.remaining()
+ if left == None:
+ return None # not sure
+ done = util.now() + left
+ return done
+
+
+ def remote_subscribe(self, remote, interval=5):
+ # [interval, timer, needUpdate]
+ # don't send an update more than once per interval
+ self.watchers[remote] = WatcherState(interval)
+ remote.notifyOnDisconnect(self.removeWatcher)
+ self.updateWatcher(remote)
+ self.startTimer(remote)
+ log.msg("BuildProgress.remote_subscribe(%s)" % remote)
+ def remote_unsubscribe(self, remote):
+ # TODO: this doesn't work. I think 'remote' will always be different
+ # than the object that appeared in _subscribe.
+ log.msg("BuildProgress.remote_unsubscribe(%s)" % remote)
+ self.removeWatcher(remote)
+ #remote.dontNotifyOnDisconnect(self.removeWatcher)
+ def removeWatcher(self, remote):
+ #log.msg("removeWatcher(%s)" % remote)
+ try:
+ timer = self.watchers[remote].timer
+ if timer:
+ timer.cancel()
+ del self.watchers[remote]
+ except KeyError:
+ log.msg("Weird, removeWatcher on non-existent subscriber:",
+ remote)
+ def sendAllUpdates(self):
+ for r in self.watchers.keys():
+ self.updateWatcher(r)
+ def updateWatcher(self, remote):
+ # an update wants to go to this watcher. Send it if we can, otherwise
+ # queue it for later
+ w = self.watchers[remote]
+ if not w.timer:
+ # no timer, so send update now and start the timer
+ self.sendUpdate(remote)
+ self.startTimer(remote)
+ else:
+ # timer is running, just mark as needing an update
+ w.needUpdate = 1
+ def startTimer(self, remote):
+ w = self.watchers[remote]
+ timer = reactor.callLater(w.interval, self.watcherTimeout, remote)
+ w.timer = timer
+ def sendUpdate(self, remote, last=0):
+ self.watchers[remote].needUpdate = 0
+ #text = self.asText() # TODO: not text, duh
+ try:
+ remote.callRemote("progress", self.remaining())
+ if last:
+ remote.callRemote("finished", self)
+ except:
+ log.deferr()
+ self.removeWatcher(remote)
+
+ def watcherTimeout(self, remote):
+ w = self.watchers.get(remote, None)
+ if not w:
+ return # went away
+ w.timer = None
+ if w.needUpdate:
+ self.sendUpdate(remote)
+ self.startTimer(remote)
+ def sendLastUpdates(self):
+ for remote in self.watchers.keys():
+ self.sendUpdate(remote, 1)
+ self.removeWatcher(remote)
+
+
+class Expectations:
+ debug = False
+ # decay=1.0 ignores all but the last build
+ # 0.9 is short time constant. 0.1 is very long time constant
+ # TODO: let decay be specified per-metric
+ decay = 0.5
+
+ def __init__(self, buildprogress):
+ """Create us from a successful build. We will expect each step to
+ take as long as it did in that build."""
+
+ # .steps maps stepname to dict2
+ # dict2 maps metricname to final end-of-step value
+ self.steps = {}
+
+ # .times maps stepname to per-step elapsed time
+ self.times = {}
+
+ for name, step in buildprogress.steps.items():
+ self.steps[name] = {}
+ for metric, value in step.progress.items():
+ self.steps[name][metric] = value
+ self.times[name] = None
+ if step.startTime is not None and step.stopTime is not None:
+ self.times[name] = step.stopTime - step.startTime
+
+ def wavg(self, old, current):
+ if old is None:
+ return current
+ if current is None:
+ return old
+ else:
+ return (current * self.decay) + (old * (1 - self.decay))
+
+ def update(self, buildprogress):
+ for name, stepprogress in buildprogress.steps.items():
+ old = self.times[name]
+ current = stepprogress.totalTime()
+ if current == None:
+ log.msg("Expectations.update: current[%s] was None!" % name)
+ continue
+ new = self.wavg(old, current)
+ self.times[name] = new
+ if self.debug:
+ print "new expected time[%s] = %s, old %s, cur %s" % \
+ (name, new, old, current)
+
+ for metric, current in stepprogress.progress.items():
+ old = self.steps[name][metric]
+ new = self.wavg(old, current)
+ if self.debug:
+ print "new expectation[%s][%s] = %s, old %s, cur %s" % \
+ (name, metric, new, old, current)
+ self.steps[name][metric] = new
+
+ def expectedBuildTime(self):
+ if None in self.times.values():
+ return None
+ #return sum(self.times.values())
+ # python-2.2 doesn't have 'sum'. TODO: drop python-2.2 support
+ s = 0
+ for v in self.times.values():
+ s += v
+ return s