#!/usr/bin/env python3
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import json
import os
import re
import subprocess
import sys
import time
import urllib.request
THIS_DIR = os.path.abspath(os.path.dirname(__file__))
CHROMIUM_REPO = os.path.abspath(os.path.join(THIS_DIR, '..', '..', '..'))
LLVM_REPO = '' # This gets filled in by main().
# This script produces the dashboard at
# https://commondatastorage.googleapis.com/chromium-browser-clang/toolchain-dashboard.html
#
# Usage:
#
# ./dashboard.py > /tmp/toolchain-dashboard.html
# gsutil.py cp -a public-read /tmp/toolchain-dashboard.html gs://chromium-browser-clang/
#TODO: Add Rust and libc++ graphs.
#TODO: Plot 30-day moving averages.
#TODO: Overview with current age of each toolchain component.
#TODO: Tables of last N rolls for each component.
#TODO: Link to next roll bug, count of blockers, etc.
def timestamp_to_str(timestamp):
'''Return a string representation of a Unix timestamp.'''
return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(timestamp))
def get_git_timestamp(repo, revision):
'''Get the Unix timestamp of a git commit.'''
out = subprocess.check_output([
'git', '-C', repo, 'show', '--date=unix', '--pretty=fuller', '--no-patch',
revision
]).decode('utf-8')
DATE_RE = re.compile(r'^CommitDate: (\d+)$', re.MULTILINE)
m = DATE_RE.search(out)
return int(m.group(1))
svn2git_dict = None
def svn2git(svn_rev):
global svn2git_dict
if not svn2git_dict:
# The JSON was generated with:
# $ ( echo '{' && git rev-list 40c47680eb2a1cb9bb7f8598c319335731bd5204 | while read commit ; do SVNREV=$(git log --format=%B -n 1 $commit | grep '^llvm-svn: [0-9]*$' | awk '{print $2 }') ; [[ ! -z '$SVNREV' ]] && echo "\"$SVNREV\": \"$commit\"," ; done && echo '}' ) | tee /tmp/llvm_svn2git.json
# and manually removing the trailing comma of the last entry.
with urllib.request.urlopen(
'https://commondatastorage.googleapis.com/chromium-browser-clang/llvm_svn2git.json'
) as url:
svn2git_dict = json.load(url)
# For branch commits, use the most recent commit to main instead.
svn2git_dict['324578'] = '93505707b6d3ec117e555c5a48adc2cc56470e38'
svn2git_dict['149886'] = '60fc2425457f43f38edf5b310551f996f4f42df8'
svn2git_dict['145240'] = '12330650f843cf7613444e345a4ecfcf06923761'
return svn2git_dict[svn_rev]
def clang_rolls():
'''Return a dict from timestamp to clang revision rolled in at that time.'''
FIRST_ROLL = 'd78457ce2895e5b98102412983a979f1896eca90'
log = subprocess.check_output([
'git', '-C', CHROMIUM_REPO, 'log', '--date=unix', '--pretty=fuller', '-p',
f'{FIRST_ROLL}..origin/main', '--', 'tools/clang/scripts/update.py',
'tools/clang/scripts/update.sh'
]).decode('utf-8')
# AuthorDate is when a commit was first authored; CommitDate (part of
# --pretty=fuller) is when a commit was last updated. We use the latter since
# it's more likely to reflect when the commit become part of upstream.
DATE_RE = re.compile(r'^CommitDate: (\d+)$')
VERSION_RE = re.compile(
r'^\+CLANG_REVISION = \'llvmorg-\d+-init-\d+-g([0-9a-f]+)\'$')
VERSION_RE_OLD = re.compile(r'^\+CLANG_REVISION = \'([0-9a-f]{10,})\'$')
# +CLANG_REVISION=125186
VERSION_RE_SVN = re.compile(r'^\+CLANG_REVISION ?= ?\'?(\d{1,6})\'?$')
rolls = {}
date = None
for line in log.splitlines():
m = DATE_RE.match(line)
if m:
date = int(m.group(1))
next
rev = None
if m := VERSION_RE.match(line):
rev = m.group(1)
elif m := VERSION_RE_OLD.match(line):
rev = m.group(1)
elif m := VERSION_RE_SVN.match(line):
rev = svn2git(m.group(1))
if rev:
assert (date)
rolls[date] = rev
date = None
return rolls
def clang_roll_ages(rolls):
'''Given a dict from timestamps to clang revisions, return a list of pairs
of timestamp string and clang revision age in days at that timestamp.'''
ages = []
def add(timestamp, rev):
ages.append((timestamp_to_str(timestamp),
(timestamp - get_git_timestamp(LLVM_REPO, rev)) / (3600 * 24)))
assert (rolls)
prev_roll_rev = None
for roll_time, roll_rev in sorted(rolls.items()):
if prev_roll_rev:
add(roll_time - 1, prev_roll_rev)
add(roll_time, roll_rev)
prev_roll_rev = roll_rev
add(time.time(), prev_roll_rev)
return ages
def print_dashboard():
rolls = clang_rolls()
ages = clang_roll_ages(rolls)
print('''
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Chromium Toolchain Dashboard</title>
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
<script type="text/javascript">
google.charts.load('current', {'packages':['corechart', 'controls']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = google.visualization.arrayToDataTable([
['Date', 'Clang'],''')
for time_str, age in ages:
print(f'[new Date("{time_str}"), {age:.1f}],')
print(''']);
var dashboard = new google.visualization.Dashboard(document.getElementById('dashboard'));
var filter = new google.visualization.ControlWrapper({
controlType: 'ChartRangeFilter',
containerId: 'filter',
options: {
filterColumnIndex: 0,
ui: { chartOptions: { interpolateNulls: true, } },
},
state: {
// Start showing roughly the last 6 months.
range: { start: new Date(Date.now() - 1000 * 3600 * 24 * 31 * 6), },
},
});
var chart = new google.visualization.ChartWrapper({
chartType: 'LineChart',
containerId: 'chart',
options: {
width: 900,
title: 'Chromium Toolchain Age Over Time',
legend: 'top',
vAxis: { title: 'Age (days)' },
interpolateNulls: true,
chartArea: {'width': '80%', 'height': '80%'},
}
});
dashboard.bind(filter, chart);
dashboard.draw(data);
}
</script>
</head>
<body>
<h1>Chromium Toolchain Dashboard (go/chrome-clang-dash)</h1>''')
print(f'<p>Last updated: {timestamp_to_str(time.time())} UTC</p>')
print('''
<div id="dashboard">
<div id="chart" style="width: 900px; height: 500px"></div>
<div id="filter" style="width: 900px; height: 50px"></div>
</div>
</body>
</html>
''')
def main():
parser = argparse.ArgumentParser(
description='Generate Chromium toolchain dashboard.')
parser.add_argument('--llvm-dir',
help='LLVM repository directory.',
default=os.path.join(CHROMIUM_REPO, '..', '..',
'llvm-project'))
args = parser.parse_args()
global LLVM_REPO
LLVM_REPO = args.llvm_dir
if not os.path.isdir(os.path.join(LLVM_REPO, '.git')):
print(f'Invalid LLVM repository path: {LLVM_REPO}')
return 1
print_dashboard()
return 0
if __name__ == '__main__':
sys.exit(main())