#!/usr/bin/env python3 # ===----------------------------------------------------------------------===## # # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception # # ===----------------------------------------------------------------------===## from typing import List, Optional, Set import argparse import json import logging import os import pathlib import subprocess import sys import time def directory_path(string): if os.path.isdir(string): return pathlib.Path(string) else: raise NotADirectoryError(string) def api(lnt_url: str, test_suite: str, endpoint: str): url = f'{lnt_url}/api/db_default/v4/{test_suite}{endpoint}' logging.debug(f'Querying {url}') result = json.loads(subprocess.check_output(['curl', '-sS', url]).decode()) return result def get_benchmarked_commits(lnt_url: str, test_suite: str, machine: str) -> Set[str]: """ Return the set of commits that have already been benchmarked on the given LNT instance, test suite and machine. """ result = api(lnt_url, test_suite, f'/machines/{machine}') commits = set() if 'runs' not in result: # there is no such machine return set() for run in result['runs']: if 'git_sha' not in run: raise ValueError(f'Found run without a git_sha field: are you using the right LNT test suite? {run}') commits.add(run['git_sha']) return commits def git_rev_list(git_repo: str, paths: List[str] = []) -> List[str]: """ Return the list of all revisions in the given Git repository. Older commits are earlier in the list. """ cli = ['git', '-C', git_repo, 'rev-list', 'origin', '--', *paths] rev_list = subprocess.check_output(cli).decode().strip().splitlines() return list(reversed(rev_list)) def git_sort_revlist(git_repo: str, commits: List[str]) -> List[str]: """ Return the list of commits sorted by their chronological order (from oldest to newest) in the provided Git repository. Items earlier in the list are older than items later in the list. """ revlist_cmd = ['git', '-C', git_repo, 'rev-list', '--no-walk'] + list(commits) revlist = subprocess.check_output(revlist_cmd, text=True).strip().splitlines() return list(reversed(revlist)) def get_all_libcxx_commits(git_repo: str) -> Set[str]: """ Return the set of commits available to benchmark for libc++: this is the list of commits that touch code in libc++ that we care to benchmark. """ logging.debug(f'Fetching {git_repo}') subprocess.check_call(['git', '-C', git_repo, 'fetch', '--quiet', 'origin']) return set(git_rev_list(git_repo, paths=['libcxx/include', 'libcxx/src'])) def main(argv): parser = argparse.ArgumentParser( prog='commit-watch', description='Watch for libc++ commits to run benchmarks on and print them to standard output.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--lnt-url', type=str, required=True, help='The URL of the LNT instance to use as the source of truth for finding already-benchmarked commits.') parser.add_argument('--test-suite', type=str, required=True, help='The name of the test suite that we are producing results for on the LNT instance.') parser.add_argument('--machine', type=str, required=True, help='The name of the machine that we are producing results for on the LNT instance.') parser.add_argument('-d', '--delay', type=int, required=False, default=60, help='The delay (in seconds) to wait before successive polls.') parser.add_argument('--git-repo', type=directory_path, default=os.getcwd(), help='Optional path to the Git repository to use. By default, the current working directory is used.') parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity level: passing the option multiple times increases the level.') args = parser.parse_args(argv) VERY_VERBOSE = logging.DEBUG - 1 if args.verbose == 1: logging.basicConfig(level=logging.INFO) elif args.verbose == 2: logging.basicConfig(level=logging.DEBUG) elif args.verbose > 2: logging.basicConfig(level=VERY_VERBOSE) last_commit: Optional[str] = None # last commit printed while True: logging.info(f'Fetching all libc++ commits in {args.git_repo}') all_commits = get_all_libcxx_commits(args.git_repo) logging.log(VERY_VERBOSE, f'Found all libc++ commits: {all_commits}') logging.info(f'Getting benchmarked commits for {args.machine} from {args.lnt_url}') benchmarked_commits = get_benchmarked_commits(args.lnt_url, args.test_suite, args.machine) logging.log(VERY_VERBOSE, f'Found benchmarked commits: {benchmarked_commits}') # Print the next commit that should be benchmarked, but only when it changes over the one # that we just printed. This ensures that we don't repeatedly print that the same benchmark # should be benchmarked, which would otherwise happen when we are running the benchmarks but # have not submitted to LNT yet. commits_to_benchmark = all_commits - benchmarked_commits if not commits_to_benchmark: logging.info('No more commits to benchmark') else: commits_to_benchmark = git_sort_revlist(args.git_repo, list(commits_to_benchmark)) most_recent = commits_to_benchmark[-1] if most_recent != last_commit: logging.info(f'Selected {most_recent}') print(most_recent, flush=True) last_commit = most_recent logging.debug(f'Waiting {args.delay}s before next poll') time.sleep(args.delay) if __name__ == '__main__': main(sys.argv[1:])