#!/usr/bin/env python # Copyright (c) 2011 Johannes Berg # # Based on an idea by Luis R. Rodriguez # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # Tired of guestimating to your bosses when the next kernel will be released? # Yeah, me too! Hope it helps. import subprocess, sys, re import dateutil.parser import time, datetime import os import argparse NUM_PREDICTIONS = 6 KERNEL_TREE='/home/johannes/sites/phb-crystal-ball/ktree' OUTPUT='/home/johannes/sites/phb-crystal-ball/www/' if os.getenv('PHB_KERNEL_TREE') != None: KERNEL_TREE=os.getenv('PHB_KERNEL_TREE') if os.getenv('PHB_OUTPUT') != None: OUTPUT=os.getenv('PHB_OUTPUT') tag_re = re.compile(r'^v(2\.6|[3-9])\.[0-9]+$') def list_tags(match, tree=None): process = subprocess.Popen(['git', 'tag', '-l', match], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, universal_newlines=True, cwd=tree) stdout = process.communicate()[0] process.wait() tags = stdout.split('\n') tags = filter(lambda x: x, tags) return tags def fetch_tags(tree=None): process = subprocess.Popen(['git', 'fetch', '-t'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, universal_newlines=True, cwd=tree) stdout = process.communicate()[0] process.wait() tag_date_cache = {} def tag_date(tag, tree=None): global tag_date_cache try: return tag_date_cache[tag] except KeyError: process = subprocess.Popen(['git', 'show', '--format=format:%x00%ai%x00', tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, universal_newlines=True, cwd=tree) stdout = process.communicate()[0] process.wait() ret = stdout.split('\0')[1] ret = dateutil.parser.parse(ret) tag_date_cache[tag] = ret return ret parser = argparse.ArgumentParser(description='PHB crystal ball') parser.add_argument('--analysis', const=True, default=False, action="store_const", help="Only perform algorithm analysis") args = parser.parse_args() ktree=KERNEL_TREE fetch_tags(ktree) tags = list_tags('v2.6.*', ktree) tags += list_tags('v3.*', ktree) tags += list_tags('v4.*', ktree) tags = filter(lambda x: tag_re.match(x) and x != 'v2.6.11', tags) # sort them, the [1:] splits off the 'v' tags.sort(key=lambda x: [int(y) for y in x[1:].split('.')]) # Remove all keys from before we started doing merge windows while tags[0] != 'v2.6.32': tags.pop(0) # Collect all the tags relevant to calculating merge window # lengths, which is the main release (like 3.15 or 4.1) and # the subsequent -rc1 that marks the end of the merge window. merge_window_tags = [] merge_window_tags.extend(tags) merge_window_tags.extend(list_tags('*-rc1', ktree)) # Need to guarantee that the tags are all sorted by date # because we need to go find the "next" -rc1 after a release # and these can be tricky like 3.19->4.0-rc1. merge_window_tags.sort(key=lambda x: tag_date(x, ktree)) # Example: The next rc for v4.0 is v4.1-rc1 def next_rc1(look_for_tag): try: idx = merge_window_tags.index(look_for_tag) tag = merge_window_tags[idx + 1] if tag.endswith('-rc1'): return tag except ValueError: pass except IndexError: pass return None #tags = tags[:-1] merge_window_nr = 0; merge_window_tot = 0; for tag in tags: td = tag_date(tag, ktree) rc1 = next_rc1(tag) if rc1 == None: continue rc1_date = tag_date(rc1, ktree) merge_window_len = rc1_date - td merge_window_nr = merge_window_nr + 1 merge_window_tot = merge_window_tot + merge_window_len.total_seconds() #print("tag: %s merge window len: '%s'" % (tag, merge_window_len)) merge_window_avg_secs = merge_window_tot/merge_window_nr merge_window_avg_delta = datetime.timedelta(0, merge_window_avg_secs) #print("merge window tot: '%s' avg: '%s'" % (merge_window_tot, merge_window_avg_secs)) #print("merge window delta: '%s'" % (merge_window_avg_delta)) dates = [tag_date(t, ktree) for t in tags] def predict(tags, dates, num_predictions=NUM_PREDICTIONS): # calculate average begin=None deltas = [] fromto = [] for idx in xrange(len(dates)): d = dates[idx] if begin is None: begin = d continue deltas.append(d - begin) fromto.append((tags[idx], begin, d)) begin = d deltasum=None for d in deltas: if deltasum is None: deltasum = d continue deltasum += d average = deltasum/len(deltas) # but really use weighted averaging ewma_avg = deltas[0] for d in deltas[1:]: ewma_avg *= 3 ewma_avg += d ewma_avg /= 4 weeks = ewma_avg / 7 weeks = datetime.timedelta(int(round(ewma_avg.days / 7.0))) def next_sunday(v): return v + datetime.timedelta((6 - v.weekday()) % 7) predictions = [] lv = tags[-1] if lv == 'v3.19': lv = 'v4.-1' last_version = lv.split('.') for next in xrange(1, num_predictions + 1): version = last_version[:-1] version.append('%d' % (int(last_version[-1]) + next)) next_release = next_sunday(dates[-1] + next * ewma_avg) next_merge = next_sunday(next_release - ewma_avg + merge_window_avg_delta) predictions.append(('.'.join(version), next_release, next_merge)) return average, fromto, predictions if args.analysis: for upto in xrange(2, len(tags)): _dummy, _dummy, p = predict(tags[:upto], dates[:upto], 1) print 'predict % 8s: %s, real %s \t-- error %s' % (p[0][0], p[0][1], dates[upto], dates[upto] - p[0][1]) sys.exit(0) average, fromto, predictions = predict(tags, dates) ### output HTML f = open(os.path.join(OUTPUT, 'index.html.tmp'), 'w') f.write("phb-crystal-ball.org") f.write("") f.write("

Predictions

") f.write("Based on the last %d kernel releases," % len(tags)) f.write("with an average development time of %s," % average) f.write("and merge window time of %s," % merge_window_avg_delta) f.write("") f.write("

For reference, the kernel releases:

") f.write("") f.write("") f.write("") f.write("") f.write("") f.write("") fromto.reverse() for v, begin, d in fromto: f.write("") f.write("" % v) f.write("" % begin.strftime('%Y-%m-%d')) f.write("" % d.strftime('%Y-%m-%d')) f.write("") f.write("
Kernel versionDeveloped fromto
%s%s%s
") f.write('') f.write('
Generated on %s' % time.strftime('%Y-%m-%d')) f.write('
source') f.write(' - iCalendar') f.write('
') f.write("") f.write("") os.rename(os.path.join(OUTPUT, 'index.html.tmp'), os.path.join(OUTPUT, 'index.html')) ### output icalendar f = open(os.path.join(OUTPUT, 'kernels.ics.tmp'), 'w') f.write('BEGIN:VCALENDAR\n') f.write('VERSION:2.0\n') f.write('PRODID:http://phb-crystal-ball.org/kernels.ics\n') f.write('METHOD:PUBLISH\n') # existing kernels for idx in xrange(len(tags)): f.write('BEGIN:VEVENT\n') f.write('UID:kernel-%s@phb-crystal-ball.org\n' % tags[idx]) f.write('SUMMARY:kernel %s\n' % tags[idx]) f.write('CLASS:PUBLIC\n') f.write(dates[idx].strftime('DTSTART:%Y%m%dT000000\n')) f.write(dates[idx].strftime('DTEND:%Y%m%dT235959\n')) f.write(dates[idx].strftime('CREATED:%Y%m%d\n')) f.write(dates[idx].strftime('LAST-MODIFIED:%Y%m%d\n')) f.write('END:VEVENT\n') # predicted kernels for v, pred, merge_closed in predictions: f.write('BEGIN:VEVENT\n') f.write('UID:merge-window-close-prediction-%s@phb-crystal-ball.org\n' % v) f.write('SUMMARY:%s merge window closes (prediction)\n' % v) f.write('CLASS:PUBLIC\n') f.write(merge_closed.strftime('DTSTART:%Y%m%dT000000\n')) f.write(merge_closed.strftime('DTEND:%Y%m%dT235959\n')) f.write(time.strftime('CREATED:%Y%m%d\n')) f.write(time.strftime('LAST-MODIFIED:%Y%m%d\n')) f.write('END:VEVENT\n') f.write('BEGIN:VEVENT\n') f.write('UID:kernel-prediction-%s@phb-crystal-ball.org\n' % v) f.write('SUMMARY:%s kernel release (prediction)\n' % v) f.write('CLASS:PUBLIC\n') f.write(pred.strftime('DTSTART:%Y%m%dT000000\n')) f.write(pred.strftime('DTEND:%Y%m%dT235959\n')) f.write(time.strftime('CREATED:%Y%m%d\n')) f.write(time.strftime('LAST-MODIFIED:%Y%m%d\n')) f.write('END:VEVENT\n') f.write('END:VCALENDAR\n') os.rename(os.path.join(OUTPUT, 'kernels.ics.tmp'), os.path.join(OUTPUT, 'kernels.ics'))