diff --git a/README b/README index 36bb489..bb4f77f 100644 --- a/README +++ b/README @@ -1,19 +1,25 @@ .. -*- mode: rst -*- -Moved from heptapod -=================== +Current buidlbot status +======================= This is the code for PyPy's buildbot, powering buildbot.pypy.org. It was -imported in April 2026 from https://foss.heptapod.net/pypy/buildbot. +imported in April 2026 from https://foss.heptapod.net/pypy/buildbot. The +service is managed via ssh into the machine, and running the +``restart_buildmaster_when_not_running`` script. It uses a python2.7 venv. Everything has been tested with builbot 0.8.8 on CPython2.7 Note you must pin "automat==20.2" "incremental==21.3.0" Testing must use "pytest<4" + Patch the source ================ -buildbot 0.8.8 is really old. There are some patches needed, see patches.patch +buildbot 0.8.8 is really old. There are some patches needed, see patches.patch. +If you are starting from a new db, you will need to also patch the installed +migrate and buildbot packages for newer sqlite3: patch-migrate.patch, +patch-buildbot.patch. How to hack the PyPy buildbot ============================== @@ -36,7 +42,7 @@ How to run the PyPy buildbot If you want to run buildbot in production, you need to make sure that the function ``pypybuildbot.util.we_are_debugging`` returns ``False`` in your environment. At the moment of writing, debugging is enabled everywhere but on -cobra. +cobra, which is not really where the buildbot runs. You still need to fill ``master/slaveinfo.py`` with the passwords of the various slaves you want to use. diff --git a/bot2/pypybuildbot/builds.py b/bot2/pypybuildbot/builds.py index 85bca87..823acea 100644 --- a/bot2/pypybuildbot/builds.py +++ b/bot2/pypybuildbot/builds.py @@ -8,6 +8,7 @@ from buildbot import locks from pypybuildbot.util import symlink_force from buildbot.status.results import SKIPPED, SUCCESS +import glob import os import json @@ -385,26 +386,13 @@ def update_hg(platform, factory, repourl, workdir, revision, use_branch, workdir=workdir, logEnviron=False)) -def update_git(platform, factory, repourl, workdir, branch='main', - alwaysUseLatest=False): - factory.addStep( - Git( - repourl=repourl, - mode='full', - method='fresh', - workdir=workdir, - branch=branch, - alwaysUseLatest=alwaysUseLatest, - timeout=40*60, - logEnviron=False)) def setup_steps(platform, factory, workdir=None, repourl='https://github.com/pypy/pypy/', force_branch=None): factory.addStep(shell.SetPropertyFromCommand( - command=['python', '-c', "import tempfile, os ;print" - " tempfile.gettempdir() + os.path.sep"], + command=['python', '-c', "import tempfile, os; print(tempfile.gettempdir() + os.path.sep)"], property="target_tmpdir", env={'TMPDIR': "${TMPDIR}"}, )) @@ -425,7 +413,14 @@ def setup_steps(platform, factory, workdir=None, revision=WithProperties("%(revision)s") # update_hg(platform, factory, repourl, workdir, revision, use_branch=True, # force_branch=force_branch, wipe_bookmarks=True) - update_git(platform, factory, repourl, workdir, branch=force_branch) + factory.addStep(Git( + repourl=repourl, + mode='full', + method='fresh', + workdir=workdir, + branch=force_branch, + timeout=40*60, + logEnviron=False)) # factory.addStep(CheckGotRevision(workdir=workdir)) @@ -893,7 +888,8 @@ def __init__(self, platform='linux', host='speed_python', postfix=''): workdir=".")) class JITBenchmark(factory.BuildFactory): - def __init__(self, platform='linux', host='benchmarker', postfix=''): + def __init__(self, platform='linux', host='benchmarker', postfix='', + upload_credentials=None): factory.BuildFactory.__init__(self) # @@ -910,7 +906,6 @@ def __init__(self, platform='linux', host='benchmarker', postfix=''): command=command, workdir='./benchmarks', haltOnFailure=False)) - # if platform in ("win32", "win64"): command = "if NOT EXIST .hg %s" else: @@ -921,28 +916,21 @@ def __init__(self, platform='linux', host='benchmarker', postfix=''): workdir='./benchmarks', timeout=3600, haltOnFailure=True)) - # self.addStep( ShellCmd(description="benchmrk: hg purge", command="hg --config extensions.purge= purge --all", workdir='./benchmarks', haltOnFailure=True)) - # self.addStep(ShellCmd(description="benchmrk: hg pull", command="hg pull %s" % repourl, workdir='./benchmarks')) - # - # update with the branch self.addStep(ShellCmd(description="benchmrk: hg update", command=Interpolate("hg update --clean %(prop:benchmark_branch)s"), workdir='./benchmarks')) - self.addStep(ShellCmd(description="benchmrk: hg report revision", command=Interpolate("hg parents --template='got_revision:{rev}:{node}'"), workdir='./benchmarks')) - - # setup_steps(platform, self) if host == 'benchmarker': lock = BenchmarkerLock @@ -951,12 +939,23 @@ def __init__(self, platform='linux', host='benchmarker', postfix=''): else: assert False, 'unknown host %s' % host - def extract_info(rc, stdout, stderr): - if rc == 0: - return json.loads(stdout) - else: - return {} - + upload_env = { + 'SPEED_UPLOAD_URL': os.environ.get('SPEED_UPLOAD_URL', 'https://speed.pypy.org/'), + 'SPEED_UPLOAD_HOST': os.environ.get('SPEED_UPLOAD_HOST', host), + } + if upload_credentials: + upload_env['SPEED_UPLOAD_USER'] = upload_credentials.get('username', '') + upload_env['SPEED_UPLOAD_PASSWORD'] = upload_credentials.get('password', '') + + def extract_upload_config(rc, stdout, stderr): + return {'upload_url': upload_env['SPEED_UPLOAD_URL'], + 'upload_host': upload_env['SPEED_UPLOAD_HOST']} + self.addStep(shell.SetPropertyFromCommand( + command=['python', '-c', 'print("ok")'], + extract_fn=extract_upload_config, + description='set upload config', + )) + self.addStep( Translate( translationArgs=['-Ojit'], @@ -966,6 +965,7 @@ def extract_info(rc, stdout, stderr): locks=[lock.access('counting')], ) ) + @renderer def get_cmd(props): # set from testrunner/get_info.py @@ -981,21 +981,174 @@ def get_cmd(props): '--changed', target, '--baseline', target, '--args', ',--jit off', - '--upload', - '--upload-executable', exe + postfix, - '--upload-project', project, - # use only the hash in the revision '--revision', rev, '--branch', branch, - '--upload-urls', 'https://speed.pypy.org/', - '--upload-baseline', - '--upload-baseline-executable', exe + '-jit' + postfix, - '--upload-baseline-project', project, - '--upload-baseline-revision', rev, - '--upload-baseline-branch', branch, - '--upload-baseline-urls', 'https://speed.pypy.org/', - ] - return command + ] + return command + + # Push bulk_upload.py to the slave so upload steps can use it + self.addStep(transfer.FileDownload( + mastersrc=os.path.join(os.path.dirname(__file__), 'bulk_upload.py'), + slavedest='bulk_upload.py', + workdir='.')) + + def _props_for_upload(props): + target = props.getProperty('target_path') + exe = os.path.split(target)[-1][:-2] + project = props.getProperty('project', default='PyPy') + rev = props.getProperty('got_revision').split(':')[-1] + branch = props.getProperty('branch') or 'main' + if branch == 'None': + branch = 'main' + return exe, project, rev, branch + + @renderer + def get_upload_changed_cmd(props): + exe, project, rev, branch = _props_for_upload(props) + return ['python3', 'bulk_upload.py', 'benchmarks/result.json', + '-e', exe + postfix, '-H', upload_env['SPEED_UPLOAD_HOST'], + '-P', project, '-r', rev, '-B', branch, + '-u', upload_env['SPEED_UPLOAD_URL']] + + @renderer + def get_upload_baseline_cmd(props): + exe, project, rev, branch = _props_for_upload(props) + return ['python3', 'bulk_upload.py', 'benchmarks/result.json', + '-e', exe + '-jit' + postfix, '-H', upload_env['SPEED_UPLOAD_HOST'], + '-P', project, '-r', rev, '-B', branch, + '-u', upload_env['SPEED_UPLOAD_URL'], + '--baseline'] + + # Pyperformance: only when the target binary is pypy3* + def is_py3_target(step): + target = step.build.getProperty('target_path') or '' + return os.path.basename(target).startswith('pypy3') + + @renderer + def get_pyperformance_venv_cmd(props): + target = props.getProperty('target_path') + return [target, '-m', 'venv', 'pyperformance_venv'] + + @renderer + def get_pyperformance_install_cmd(props): + spec = props.getProperty('pyperformance_spec', default='pyperformance').strip() + return ['./pyperformance_venv/bin/pip', 'install', '--upgrade', spec] + + @renderer + def get_pyperformance_bench_venv_cmd(props): + target = props.getProperty('target_path') + return ['./pyperformance_venv/bin/python', '-m', 'pyperformance', + 'venv', 'recreate', '-p', target] + + def get_pyperformance_run_cmd(outfile, inherit_environ=None, fast=False): + @renderer + def _cmd(props): + target = props.getProperty('target_path') + inherit = ('--inherit-environ %s ' % inherit_environ + if inherit_environ else '') + fast_flag = '-f ' if fast else '' + return ['bash', '-c', + 'rm -f %s && ' + './pyperformance_venv/bin/python -m pyperformance run ' + '%s%s--python %s --output %s' % (outfile, fast_flag, inherit, target, outfile)] + return _cmd + + @renderer + def get_pyperformance_upload_cmd(props): + exe, project, rev, branch = _props_for_upload(props) + return ['python3', 'bulk_upload.py', 'pyperformance_result.json', + '-e', exe + '-jit' + postfix, '-H', upload_env['SPEED_UPLOAD_HOST'], + '-P', project, '-r', rev, '-B', branch, + '-u', upload_env['SPEED_UPLOAD_URL']] + + @renderer + def get_pyperformance_nojit_upload_cmd(props): + exe, project, rev, branch = _props_for_upload(props) + return ['python3', 'bulk_upload.py', 'pyperformance_nojit_result.json', + '-e', exe + postfix, '-H', upload_env['SPEED_UPLOAD_HOST'], + '-P', project, '-r', rev, '-B', branch, + '-u', upload_env['SPEED_UPLOAD_URL']] + + self.addStep(ShellCmd( + description='clean up old pypy venvs', + command=['sh', '-c', 'rm -rf venv pyperformance_venv'], + doStepIf=is_py3_target, + workdir='.')) + self.addStep(ShellCmd( + description='create pyperformance venv', + command=get_pyperformance_venv_cmd, + doStepIf=is_py3_target, + workdir='.')) + self.addStep(ShellCmd( + description='install pyperformance', + command=get_pyperformance_install_cmd, + doStepIf=is_py3_target, + workdir='.')) + self.addStep(ShellCmd( + description='create pyperformance benchmark venv', + command=get_pyperformance_bench_venv_cmd, + doStepIf=is_py3_target, + workdir='.')) + + # Transfer all PyPy-compatibility patch scripts from master to worker, + # then apply them in a single step. + _patches_dir = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..', 'patches')) + for _patch_file in sorted(glob.glob( + os.path.join(_patches_dir, 'patch_*_pypy.py'))): + self.addStep(transfer.FileDownload( + mastersrc=_patch_file, + slavedest=os.path.basename(_patch_file), + doStepIf=is_py3_target, + workdir='.')) + self.addStep(ShellCmd( + description='apply PyPy compatibility patches', + command=['python3', '-c', + 'import glob, os, subprocess, sys\n' + 'print("cwd:", os.getcwd())\n' + 'print("cwd contents:", sorted(os.listdir(".")))\n' + 'venv_dir = "venv"\n' + 'if os.path.isdir(venv_dir):\n' + ' print("venv/ contents:", sorted(os.listdir(venv_dir)))\n' + 'else:\n' + ' print("venv/ directory does not exist")\n' + 'scripts = sorted(glob.glob("patch_*_pypy.py"))\n' + 'print("Applying patches:", scripts)\n' + 'for f in scripts:\n' + ' subprocess.check_call([sys.executable, f])\n'], + doStepIf=is_py3_target, + haltOnFailure=True, + workdir='.')) + + self.addStep(ShellCmd( + description='run pyperformance (jit)', + command=get_pyperformance_run_cmd('pyperformance_result.json'), + locks=[lock.access('exclusive')], + doStepIf=is_py3_target, + workdir='.', + timeout=7200)) + self.addStep(ShellCmd( + description='upload pyperformance results (jit)', + command=get_pyperformance_upload_cmd, + env=upload_env, + doStepIf=is_py3_target, + workdir='.')) + self.addStep(ShellCmd( + description='run pyperformance (nojit)', + command=get_pyperformance_run_cmd('pyperformance_nojit_result.json', + inherit_environ='PYPY_DISABLE_JIT', + fast=True), + env={'PYPY_DISABLE_JIT': '1'}, + locks=[lock.access('exclusive')], + doStepIf=is_py3_target, + workdir='.', + timeout=7200)) + self.addStep(ShellCmd( + description='upload pyperformance results (nojit)', + command=get_pyperformance_nojit_upload_cmd, + env=upload_env, + doStepIf=is_py3_target, + workdir='.')) self.addStep(ShellCmd( # this step needs exclusive access to the CPU @@ -1004,12 +1157,33 @@ def get_cmd(props): command=get_cmd, workdir='./benchmarks', timeout=3600)) - # a bit obscure hack to get both os.path.expand and a property + self.addStep(ShellCmd( + description='upload legacy results (jit-off)', + command=get_upload_changed_cmd, + env=upload_env, + workdir='.')) + self.addStep(ShellCmd( + description='upload legacy results (jit-on)', + command=get_upload_baseline_cmd, + env=upload_env, + workdir='.')) + + # Archive the legacy result file on the master filename = '%(got_revision)s' + (postfix or '') resfile = os.path.expanduser("~/bench_results/%s.json" % filename) self.addStep(transfer.FileUpload(slavesrc="benchmarks/result.json", masterdest=WithProperties(resfile), workdir=".")) + pyresfile = os.path.expanduser("~/bench_results/%s-pyperformance.json" % filename) + self.addStep(transfer.FileUpload(slavesrc="pyperformance_result.json", + masterdest=WithProperties(pyresfile), + doStepIf=is_py3_target, + workdir=".")) + pynoresfile = os.path.expanduser("~/bench_results/%s-pyperformance-nojit.json" % filename) + self.addStep(transfer.FileUpload(slavesrc="pyperformance_nojit_result.json", + masterdest=WithProperties(pynoresfile), + doStepIf=is_py3_target, + workdir=".")) class CPythonBenchmark(factory.BuildFactory): @@ -1195,10 +1369,15 @@ def __init__(self, platform='linux', # obtain a pypy-compatible branch of numpy numpy_url = 'https://foss.heptapod.net/pypy/numpy' - update_git(platform, self, numpy_url, 'numpy_src', branch='master', - alwaysUseLatest=True, # ignore pypy rev number when - # triggered by a pypy build - ) + self.addStep(Git( + repourl=numpy_url, + mode='full', + method='fresh', + workdir='numpy_src', + branch='master', + alwaysUseLatest=True, + timeout=40*60, + logEnviron=False)) self.addStep(ShellCmd( description="install numpy", diff --git a/bot2/pypybuildbot/bulk_upload.py b/bot2/pypybuildbot/bulk_upload.py new file mode 100644 index 0000000..8c75d09 --- /dev/null +++ b/bot2/pypybuildbot/bulk_upload.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +Bulk upload benchmark results to a codespeed instance in a single POST. + +Supports two input formats: + legacy - result.json produced by runner.py (unladen_swallow/perf format) + pyperf - result.json produced by pyperformance (pyperf format) + +Revision and branch are read from the JSON file when possible (legacy format); +pass --revision / --branch on the command line to override or when using pyperf. + +Examples: + # legacy, jit-off results + ./bulk_upload.py result.json -e pypy-c-64 -H benchmarker + + # legacy, jit-on results (avg_base values) + ./bulk_upload.py result.json -e pypy-c-jit-64 -H benchmarker --baseline + + # pyperformance + ./bulk_upload.py pyperf.json -e pypy-c-64 -H benchmarker -r abc123 -B py3.11 +""" +import argparse +import json +import os +import statistics +import sys +import time +import urllib.error +import urllib.parse +import urllib.request + + +def detect_format(data): + if 'benchmarks' in data and isinstance(data.get('benchmarks'), list): + benchmarks = data['benchmarks'] + if benchmarks and 'runs' in benchmarks[0]: + return 'pyperf' + if 'results' in data: + return 'legacy' + raise ValueError("Unrecognised result file format") + + +def parse_legacy(data, changed): + """Return list of (name, value, std_dev) from a runner.py result.json. + + changed=True -> upload avg_changed values (jit-off run, executable pypy-c-NN) + changed=False -> upload avg_base values (jit-on run, executable pypy-c-jit-NN) + """ + records = [] + for name, result_type, result_data in data['results']: + value = std_dev = None + + if result_type == 'ComparisonResult': + if changed: + value = result_data.get('avg_changed') + std_dev = result_data.get('std_changed') + else: + value = result_data.get('avg_base') + std_dev = result_data.get('std_base') + + elif result_type == 'SimpleComparisonResult': + value = result_data['changed_time'] if changed else result_data['base_time'] + + elif result_type == 'RawResult': + times = result_data['changed_times'] if changed else result_data['base_times'] + if times: + value = times[0] if len(times) == 1 else statistics.mean(times) + std_dev = statistics.stdev(times) if len(times) > 1 else None + + if not value: + continue + records.append((name, value, std_dev)) + return records + + +def parse_pyperf(data): + """Return (records, suite_version) from a pyperformance/pyperf result.json. + + records is a list of (name, mean_seconds, std_dev_or_None). + suite_version is taken from the first benchmark's metadata 'version' field. + """ + records = [] + suite_version = '' + + for bench in data.get('benchmarks', []): + meta = bench.get('metadata', {}) + name = meta.get('name', '') + if not name: + continue + if not suite_version: + suite_version = meta.get('version', '') + + # pyperf stores values as floats (seconds) inside each run. + # Each value entry is either a plain float or a (loops, time_per_loop) pair. + values = [] + for run in bench.get('runs', []): + for v in run.get('values', []): + values.append(v[1] if isinstance(v, (list, tuple)) else v) + + if not values: + continue + + mean = statistics.mean(values) + std_dev = statistics.stdev(values) if len(values) > 1 else None + records.append((name, mean, std_dev)) + + return records, suite_version + + +def build_codespeed_record(name, value, std_dev, args, + source='legacy', suite_version=''): + record = { + 'commitid': args.revision, + 'branch': args.branch, + 'project': args.project, + 'executable': args.executable, + 'environment': args.host, + 'benchmark': name, + 'result_value': value, + 'source': source, + } + if std_dev is not None: + record['std_dev'] = std_dev + if suite_version: + record['suite_version'] = suite_version + return record + + +def send_bulk(records, url, username=None, password=None): + params = urllib.parse.urlencode({'json': json.dumps(records)}).encode('utf-8') + + if username and password: + mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() + mgr.add_password(None, url, username, password) + opener = urllib.request.build_opener(urllib.request.HTTPBasicAuthHandler(mgr)) + else: + opener = urllib.request.build_opener() + + retries = [1, 3, 6, 20] + while True: + try: + req = urllib.request.Request(url + 'result/add/json/', params) + resp = opener.open(req) + print(resp.read().decode()) + return + except urllib.error.HTTPError as e: + body = e.read().decode(errors='replace') + print(f"Upload failed with HTTP {e.code}: {body}") + if e.code < 500: + sys.exit(1) + if not retries: + sys.exit(1) + delay = retries.pop(0) + print(f"Retrying in {delay}s...") + time.sleep(delay) + except urllib.error.URLError: + if not retries: + raise + delay = retries.pop(0) + print(f"Upload failed, retrying in {delay}s...") + time.sleep(delay) + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument('jsonfile', help='Path to result JSON file') + parser.add_argument('-e', '--executable', required=True, + help='Executable name in codespeed') + parser.add_argument('-H', '--host', required=True, + help='Environment/host name in codespeed') + parser.add_argument('-r', '--revision', default=None, + help='Commit id (read from file if omitted)') + parser.add_argument('-B', '--branch', default=None, + help='Branch name (read from file if omitted)') + parser.add_argument('-P', '--project', default='PyPy', + help='Project name in codespeed (default: PyPy)') + parser.add_argument('-u', '--url', default='https://speed.pypy.org/', + help='Base URL of codespeed instance') + parser.add_argument('-b', '--baseline', action='store_true', + help='Upload avg_base values (legacy format only; ' + 'use for the jit-on executable)') + args = parser.parse_args() + + with open(args.jsonfile) as f: + data = json.load(f) + + fmt = detect_format(data) + + if args.revision is None: + args.revision = data.get('revision') or '' + if args.branch is None: + args.branch = data.get('branch') or 'default' + + if not args.revision: + parser.error('--revision is required (could not read from file)') + + if not args.url.endswith('/'): + args.url += '/' + + if fmt == 'legacy': + raw = parse_legacy(data, changed=not args.baseline) + records = [ + build_codespeed_record(name, value, std_dev, args, source='legacy') + for name, value, std_dev in raw + ] + else: + raw, suite_version = parse_pyperf(data) + records = [ + build_codespeed_record(name, value, std_dev, args, + source='pyperformance', + suite_version=suite_version) + for name, value, std_dev in raw + ] + + if not records: + print("No results to upload.", file=sys.stderr) + sys.exit(1) + + username = os.environ.get('SPEED_UPLOAD_USER') + password = os.environ.get('SPEED_UPLOAD_PASSWORD') + auth_note = f" as {username!r}" if username and password else " (no credentials)" + + print(f"Uploading {len(records)} results ({fmt} format) " + f"for {args.executable!r} env {args.host!r} to {args.url}{auth_note}") + send_bulk(records, args.url, username=username, password=password) + print("Done.") + + +if __name__ == '__main__': + main() diff --git a/bot2/pypybuildbot/master.py b/bot2/pypybuildbot/master.py index bf01870..72711d6 100644 --- a/bot2/pypybuildbot/master.py +++ b/bot2/pypybuildbot/master.py @@ -27,14 +27,25 @@ def force(self, owner, builder_name, **kwargs): return ForceScheduler.force(self, owner, builder_name, **kwargs) +import re as _re +_PYPERFORMANCE_SPEC_RE = _re.compile( + r'^(' + r'pyperformance\s*([><=!~^][^;]*)?' # PyPI: pyperformance, pyperformance==1.2 + r'|git\+https://github\.com/python/pyperformance(@\S+)?' # GitHub: python/pyperformance only + r')$' +) + class BenchmarkForceScheduler(CustomForceScheduler): ''' - A ForceScheduler with an extra field: benchmark_branch + A ForceScheduler with extra fields: benchmark_branch and pyperformance_spec ''' def __init__(self, name, builderNames, benchmark_branch=StringParameter(name="benchmark_branch", - label="Benchmark repo branch:", + label="Legacy benchmark repo branch:", default="default", length=20), + pyperformance_spec=StringParameter(name="pyperformance_spec", + label="pyperformance pip specifier:", + default="pyperformance", length=60), properties=[ CodebaseParameter('PyPy repo', label='PyPy Repo')], **kwargs): CustomForceScheduler.__init__(self, name, builderNames, **kwargs) @@ -45,8 +56,19 @@ def __init__(self, name, builderNames, pypy_branch) self.all_fields.append(benchmark_branch) self.forcedProperties.append(benchmark_branch) + self.all_fields.append(pyperformance_spec) + self.forcedProperties.append(pyperformance_spec) def force(self, owner, builderNames=None, **kwargs): + spec = kwargs.get('pyperformance_spec', 'pyperformance') + if isinstance(spec, list): + spec = spec[0] if spec else 'pyperformance' + spec = spec.strip() + if not _PYPERFORMANCE_SPEC_RE.match(spec): + raise ValidationError( + "pyperformance pip specifier must be 'pyperformance', " + "'pyperformance==', or " + "'git+https://github.com/python/pyperformance@'") CustomForceScheduler.force(self, owner, builderNames, **kwargs) # Forbid "stop build" without a reason that starts with "!" @@ -239,10 +261,12 @@ def _checkStopBuild(self, reason=""): pypyjit=True, app_tests=True) -pypyJITBenchmarkFactory = pypybuilds.JITBenchmark(host='benchmarker') +pypyJITBenchmarkFactory = pypybuilds.JITBenchmark(host='benchmarker', + upload_credentials=upload_credentials) pypyJITBenchmarkFactory64 = pypybuilds.JITBenchmark(platform='linux64', host='benchmarker', - postfix='-64') + postfix='-64', + upload_credentials=upload_credentials) pypyJITBenchmarkFactory64_speed = pypybuilds.JITBenchmarkSingleRun( platform='linux64', host='speed_python', diff --git a/bot2/slaveinfo.py b/bot2/slaveinfo.py index aeccc4a..b11daf5 100644 --- a/bot2/slaveinfo.py +++ b/bot2/slaveinfo.py @@ -1,3 +1,6 @@ # Mapping from slave name to slave password -passwords = {} +passwords = {'localhost': 'localhost'} + +# Credentials for uploading benchmark results to speed.pypy.org +upload_credentials = {'username': '', 'password': ''} diff --git a/master/master.cfg b/master/master.cfg index 54b0ffb..99fa4e4 100644 --- a/master/master.cfg +++ b/master/master.cfg @@ -19,6 +19,8 @@ httpPortNumber = 8099 slaveinfo = load('slaveinfo') passwords = slaveinfo.passwords +upload_credentials = getattr(slaveinfo, 'upload_credentials', None) + execfile(os.path.join(botdir, 'pypybuildbot', 'master.py')) if we_are_debugging(): diff --git a/patch-buildbot.patch b/patch-buildbot.patch new file mode 100644 index 0000000..230616e --- /dev/null +++ b/patch-buildbot.patch @@ -0,0 +1,43 @@ +25,26c25,50 +< sourcestamps_table = sa.Table('sourcestamps', metadata, autoload=True) +< buildsets_table = sa.Table('buildsets', metadata, autoload=True) +--- +> # Define tables explicitly to avoid SQLite reflection issues caused by +> # migration_tmp references left in sqlite_master by migration 004's ALTER TABLE. +> sa.Table('patches', metadata, +> sa.Column('id', sa.Integer, primary_key=True), +> sa.Column('patchlevel', sa.Integer, nullable=False), +> sa.Column('patch_base64', sa.Text, nullable=False), +> sa.Column('subdir', sa.Text), +> ) +> sourcestamps_table = sa.Table('sourcestamps', metadata, +> sa.Column('id', sa.Integer, primary_key=True), +> sa.Column('branch', sa.String(256)), +> sa.Column('revision', sa.String(256)), +> sa.Column('patchid', sa.Integer, sa.ForeignKey('patches.id')), +> sa.Column('repository', sa.Text, nullable=False, server_default=''), +> sa.Column('project', sa.Text, nullable=False, server_default=''), +> ) +> buildsets_table = sa.Table('buildsets', metadata, +> sa.Column('id', sa.Integer, primary_key=True), +> sa.Column('external_idstring', sa.String(256)), +> sa.Column('reason', sa.String(256)), +> sa.Column('sourcestampid', sa.Integer, sa.ForeignKey('sourcestamps.id'), nullable=False), +> sa.Column('submitted_at', sa.Integer, nullable=False), +> sa.Column('complete', sa.SmallInteger, nullable=False, server_default=sa.DefaultClause("0")), +> sa.Column('complete_at', sa.Integer), +> sa.Column('results', sa.SmallInteger), +> ) +45c69,78 +< buildsets_table = sa.Table('buildsets', metadata, autoload=True) +--- +> buildsets_table = sa.Table('buildsets', metadata, +> sa.Column('id', sa.Integer, primary_key=True), +> sa.Column('external_idstring', sa.String(256)), +> sa.Column('reason', sa.String(256)), +> sa.Column('sourcestampsetid', sa.Integer, sa.ForeignKey('sourcestampsets.id'), nullable=False), +> sa.Column('submitted_at', sa.Integer, nullable=False), +> sa.Column('complete', sa.SmallInteger, nullable=False, server_default=sa.DefaultClause("0")), +> sa.Column('complete_at', sa.Integer), +> sa.Column('results', sa.SmallInteger), +> ) diff --git a/patch-migrate.patch b/patch-migrate.patch new file mode 100644 index 0000000..29bbee2 --- /dev/null +++ b/patch-migrate.patch @@ -0,0 +1,18 @@ +29a30,37 +> # SQLite 3.26.0+ propagates RENAME TABLE into FK references in other +> # tables, which breaks the migration_tmp pattern. legacy_alter_table +> # restores the pre-3.26.0 behaviour for the duration of this rename. +> try: +> self.connection.execute('PRAGMA legacy_alter_table = ON') +> except Exception: +> pass +> +45c53,58 +< +--- +> +> try: +> self.connection.execute('PRAGMA legacy_alter_table = OFF') +> except Exception: +> pass +> diff --git a/patches/patch_bm_pickle_pypy.py b/patches/patch_bm_pickle_pypy.py new file mode 100644 index 0000000..410c171 --- /dev/null +++ b/patches/patch_bm_pickle_pypy.py @@ -0,0 +1,63 @@ +""" +Patch pyperformance bm_pickle for PyPy compatibility. + +PyPy provides a _pickle accelerator module, so the existing IS_PYPY check +in the pure-python guard incorrectly raises RuntimeError. Move IS_PYPY to +the accelerated-module check instead so PyPy can use its C extension. + +Source: https://github.com/python/pyperformance/pull/461 +""" +import glob +import os +import sys + +GLOBS = [ + "./*/lib/*/site-packages/pyperformance/data-files/benchmarks/bm_pickle/run_benchmark.py", + "./*/lib/*/site-packages/benchmarks/bm_pickle/run_benchmark.py", + "./*/*/lib/*/site-packages/pyperformance/data-files/benchmarks/bm_pickle/run_benchmark.py", + "./*/*/lib/*/site-packages/benchmarks/bm_pickle/run_benchmark.py", +] + +# Deduplicate by real path in case any entries are symlinks to the same file +seen = {} +for pattern in GLOBS: + for path in glob.glob(pattern): + seen[os.path.realpath(path)] = path +files = list(seen.values()) + +if not files: + print("ERROR: bm_pickle run_benchmark.py not found – patch failed") + sys.exit(1) + +OLD_PURE = "if not (options.pure_python or IS_PYPY):" +NEW_PURE = "if not (options.pure_python):" +OLD_ACCEL = "if not is_accelerated_module(pickle):" +NEW_ACCEL = "if not is_accelerated_module(pickle) and not IS_PYPY:" + +failed = False +for path in files: + txt = open(path).read() + original = txt + + if OLD_PURE in txt: + txt = txt.replace(OLD_PURE, NEW_PURE) + elif NEW_PURE in txt: + print(f"NOTE: {path} pure_python guard already patched") + else: + print(f"ERROR: {path} pure_python guard pattern not found – version mismatch?") + failed = True + + if OLD_ACCEL in txt: + txt = txt.replace(OLD_ACCEL, NEW_ACCEL) + elif NEW_ACCEL in txt: + print(f"NOTE: {path} accelerated-module guard already patched") + else: + print(f"ERROR: {path} accelerated-module guard pattern not found – version mismatch?") + failed = True + + if txt != original: + open(path, "w").write(txt) + print(f"Patched {path}") + +if failed: + sys.exit(1) diff --git a/patches/patch_sqlalchemy_identity_pypy.py b/patches/patch_sqlalchemy_identity_pypy.py new file mode 100644 index 0000000..a4e819e --- /dev/null +++ b/patches/patch_sqlalchemy_identity_pypy.py @@ -0,0 +1,65 @@ +""" +Patch bm_sqlalchemy_declarative for PyPy GC compatibility. + +SQLite reuses primary key IDs after a full table DELETE; the next loop +iteration inserts new objects with the same IDs as the previous run. On +CPython, refcounting immediately collects the old objects, clearing the +identity map's weakrefs. On PyPy, GC is deferred, so the old objects +remain alive and SQLAlchemy emits a SAWarning for every inserted row on +every loop, flooding stderr and causing the benchmark to fail. + +Fix: call session.expunge_all() after the bulk deletes to explicitly clear +stale session state before timing begins. + +Source: https://github.com/python/pyperformance/pull/472 +""" +import glob +import os +import sys + +GLOBS = [ + "./*/lib/*/site-packages/pyperformance/data-files/benchmarks/bm_sqlalchemy_declarative/run_benchmark.py", + "./*/lib/*/site-packages/benchmarks/bm_sqlalchemy_declarative/run_benchmark.py", + "./*/*/lib/*/site-packages/pyperformance/data-files/benchmarks/bm_sqlalchemy_declarative/run_benchmark.py", + "./*/*/lib/*/site-packages/benchmarks/bm_sqlalchemy_declarative/run_benchmark.py", +] + +seen = {} +for pattern in GLOBS: + for path in glob.glob(pattern): + seen[os.path.realpath(path)] = path +files = list(seen.values()) + +if not files: + print("ERROR: bm_sqlalchemy_declarative/run_benchmark.py not found – patch failed") + sys.exit(1) + +OLD = ( + " session.query(Person).delete(synchronize_session=False)\n" + " session.query(Address).delete(synchronize_session=False)\n" +) +NEW = ( + " session.query(Person).delete(synchronize_session=False)\n" + " session.query(Address).delete(synchronize_session=False)\n" + " session.expunge_all()\n" +) + +failed = False +for path in files: + txt = open(path).read() + original = txt + + if OLD in txt: + txt = txt.replace(OLD, NEW) + elif NEW in txt: + print(f"NOTE: {path} already patched") + else: + print(f"ERROR: {path} pattern not found – version mismatch?") + failed = True + + if txt != original: + open(path, "w").write(txt) + print(f"Patched {path}") + +if failed: + sys.exit(1)