Source code for asv.commands.publish

# Licensed under a 3-clause BSD style license - see LICENSE.rst
import datetime
import multiprocessing
import os
import shutil
from collections import defaultdict

from importlib_metadata import version as get_version

from asv import _stats, util
from asv.benchmarks import Benchmarks
from asv.commands import Command
from asv.console import log
from asv.graph import GraphSet
from asv.machine import iter_machine_files
from asv.publishing import OutputPublisher
from asv.repo import get_repo
from asv.results import iter_results


[docs] def check_benchmark_params(name, benchmark): """ Check benchmark params and param_keys items, so that the javascript can assume this data is valid. It is checked in benchmark.py already when it is generated, but best to double check in any case. """ if 'params' not in benchmark: # Old-format benchmarks.json benchmark['params'] = [] benchmark['param_names'] = [] msg = f"Information in benchmarks.json for benchmark {name} is malformed" if not isinstance(benchmark['params'], list) or not isinstance(benchmark['param_names'], list): raise ValueError(msg) if len(benchmark['params']) != len(benchmark['param_names']): raise ValueError(msg) for item in benchmark['params']: if not isinstance(item, list): raise ValueError(msg)
[docs] class Publish(Command): @classmethod
[docs] def setup_arguments(cls, subparsers): parser = subparsers.add_parser( "publish", help="Collate results into a website", description=""" Collate all results into a website. This website will be written to the ``html_dir`` given in the ``asv.conf.json`` file, and may be served using any static web server.""", ) parser.add_argument( '--no-pull', action='store_true', dest='no_pull', help="Do not pull the repository" ) parser.add_argument( 'range', nargs='?', default=None, help="""Optional commit range to consider""" ) parser.add_argument( '--html-dir', '-o', default=None, help=("Optional output directory. Default is 'html_dir' from asv config"), ) parser.set_defaults(func=cls.run_from_args) return parser
@classmethod
[docs] def run_from_conf_args(cls, conf, args): if args.html_dir is not None: conf.html_dir = args.html_dir return cls.run(conf=conf, range_spec=args.range, pull=not args.no_pull)
@staticmethod
[docs] def iter_results(conf, repo, range_spec=None): if range_spec is not None: if isinstance(range_spec, list): hashes = range_spec else: hashes = repo.get_hashes_from_range(range_spec) else: hashes = None for result in iter_results(conf.results_dir): if hashes is None or result.commit_hash in hashes: yield result
@classmethod
[docs] def run(cls, conf, range_spec=None, pull=True): params = {} env_vars = defaultdict(set) graphs = GraphSet() machines = {} benchmark_names = set() log.set_nitems(6 + len(list(util.iter_subclasses(OutputPublisher)))) if os.path.exists(conf.html_dir): util.long_path_rmtree(conf.html_dir) repo = get_repo(conf) benchmarks = Benchmarks.load(conf) def copy_ignore(src, names): # Copy only *.js and *.css in vendor dir ignore = [ fn for fn in names if ( os.path.basename(src).lower() == 'vendor' and not fn.lower().endswith('.js') and not fn.lower().endswith('.css') ) ] return ignore template_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'www') shutil.copytree(template_dir, conf.html_dir, ignore=copy_ignore) # Ensure html_dir is writable even if template_dir is on a read-only FS os.chmod(conf.html_dir, 0o755) for pre, ds, fs in os.walk(conf.html_dir): for x in fs: os.chmod(os.path.join(pre, x), 0o644) for x in ds: os.chmod(os.path.join(pre, x), 0o755) log.step() log.info("Loading machine info") with log.indent(): for path in iter_machine_files(conf.results_dir): d = util.load_json(path) machines[d['machine']] = d log.step() log.info("Getting params, commits, tags and branches") with log.indent(): # Determine first the set of all parameters and all commits hash_to_date = {} for results in cls.iter_results(conf, repo, range_spec): hash_to_date[results.commit_hash] = results.date for key, val in results.params.items(): if val is None: # Backward compatibility -- null means '' val = '' params.setdefault(key, set()) params[key].add(val) for name, val in results.env_vars.items(): # Prefix them in case of name collision env_vars[f"env-{name}"].add(val) params.update(env_vars) if pull: repo.pull() tags = repo.get_tags() revisions = repo.get_revisions(set(hash_to_date.keys()) | set(tags.values())) for tag, commit_hash in list(tags.items()): # Map to revision number instead of commit hash and add tags to hash_to_date tags[tag] = revisions[tags[tag]] hash_to_date[commit_hash] = repo.get_date_from_name(commit_hash) revision_to_date = {r: hash_to_date[h] for h, r in revisions.items()} branches = {branch: repo.get_branch_commits(branch) for branch in conf.branches} log.step() log.info("Loading results") with log.indent(): # Generate all graphs for results in cls.iter_results(conf, repo, range_spec): log.dot() branches_for_commit = [ branch for branch, commits in branches.items() if results.commit_hash in commits ] # Print a warning message if the commit isn't from a tag if not len(branches_for_commit): # Assume that these must be tags repo_tags = repo.get_tags() branches_for_commit = [ branch for branch, commits in branches.items() if results.commit_hash in repo_tags.values() ] if not len(branches_for_commit): # Not tags, print a warning msg = "Couldn't find {} in branches ({})" log.warning( msg.format( results.commit_hash[: conf.hash_length], ", ".join(str(branch) for branch in branches.keys()), ) ) for key in results.get_result_keys(benchmarks): b = benchmarks[key] b_params = b['params'] result = results.get_result_value(key, b_params) weight = [ _stats.get_weight(s) for s in results.get_result_stats(key, b_params) ] if not b_params: result = result[0] weight = weight[0] benchmark_names.add(key) for branch in branches_for_commit: cur_params = dict(results.params) cur_env = {f'env-{name}': val for name, val in results.env_vars.items()} cur_params.update(cur_env) cur_params['branch'] = repo.get_branch_name(branch) # Backward compatibility, see above for param_key, param_value in list(cur_params.items()): if param_value is None: cur_params[param_key] = '' # Fill in missing params for param_key in params.keys(): if param_key not in cur_params: cur_params[param_key] = None params[param_key].add(None) # Create graph graph = graphs.get_graph(key, cur_params) graph.add_data_point(revisions[results.commit_hash], result, weight) # Get the parameter sets for all graphs graph_param_list = [] for path, graph in graphs: if 'summary' not in graph.params: if graph.params not in graph_param_list: graph_param_list.append(graph.params) log.step() log.info("Detecting steps") with log.indent(): n_processes = multiprocessing.cpu_count() pool = util.get_multiprocessing_pool(n_processes) try: graphs.detect_steps(pool, dots=log.dot) pool.close() pool.join() finally: pool.terminate() log.step() log.info("Generating graphs") with log.indent(): # Save files graphs.save(conf.html_dir, dots=log.dot) pages = [] classes = sorted(util.iter_subclasses(OutputPublisher), key=lambda cls: cls.order) for cls in classes: log.step() log.info(f"Generating output for {cls.__name__}") with log.indent(): cls.publish(conf, repo, benchmarks, graphs, revisions) pages.append([cls.name, cls.button_label, cls.description]) log.step() log.info("Writing index") benchmark_map = dict(benchmarks) for key in benchmark_map.keys(): check_benchmark_params(key, benchmark_map[key]) for key, val in params.items(): val = list(val) val.sort(key=lambda x: '[none]' if x is None else str(x)) params[key] = val params['branch'] = [repo.get_branch_name(branch) for branch in conf.branches] revision_to_hash = {r: h for h, r in revisions.items()} util.write_json( os.path.join(conf.html_dir, "index.json"), { 'project': conf.project, 'project_url': conf.project_url, 'show_commit_url': conf.show_commit_url, 'hash_length': conf.hash_length, 'revision_to_hash': revision_to_hash, 'revision_to_date': revision_to_date, 'params': params, 'graph_param_list': graph_param_list, 'benchmarks': benchmark_map, 'machines': machines, 'tags': tags, 'pages': pages, }, compact=True, ) util.write_json( os.path.join(conf.html_dir, "info.json"), { 'asv-version': get_version("asv"), 'timestamp': util.datetime_to_js_timestamp( datetime.datetime.now(datetime.timezone.utc) ), }, )