| # Copyright 2018 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Recipe for comparing performance between two revisions of Fuchsia.""" |
| |
| from recipe_engine.config import Enum, List, Single |
| from recipe_engine.recipe_api import Property |
| |
| TARGETS = ['arm64', 'x64'] |
| |
| BUILD_TYPES = ['debug', 'release', 'thinlto', 'lto'] |
| |
| DEVICES = [ |
| 'QEMU', |
| 'Intel NUC Kit NUC6i3SYK', |
| 'Intel NUC Kit NUC7i5DNHE', |
| 'Khadas Vim2 Max', |
| ] |
| |
| DEPS = [ |
| 'fuchsia/artifacts', |
| 'fuchsia/build', |
| 'fuchsia/buildbucket_util', |
| 'fuchsia/checkout', |
| 'fuchsia/fuchsia', |
| 'fuchsia/testing', |
| 'fuchsia/testing_requests', |
| 'recipe_engine/buildbucket', |
| 'recipe_engine/context', |
| 'recipe_engine/file', |
| 'recipe_engine/json', |
| 'recipe_engine/path', |
| 'recipe_engine/properties', |
| 'recipe_engine/python', |
| 'recipe_engine/raw_io', |
| 'recipe_engine/step', |
| 'recipe_engine/time', |
| ] |
| |
| PROPERTIES = { |
| 'manifest': |
| Property(kind=str, help='Jiri manifest to use'), |
| 'remote': |
| Property(kind=str, help='Remote manifest repository'), |
| 'target': |
| Property(kind=Enum(*TARGETS), help='Target to build'), |
| 'build_type': |
| Property( |
| kind=Enum(*BUILD_TYPES), help='The build type', default='debug'), |
| 'packages': |
| Property(kind=List(basestring), help='Packages to build', default=[]), |
| 'variants': |
| Property( |
| kind=List(basestring), |
| help='--variant arguments to GN in `select_variant`', |
| default=[]), |
| 'gn_args': |
| Property( |
| kind=List(basestring), help='Extra args to pass to GN', default=[]), |
| 'ninja_targets': |
| Property( |
| kind=List(basestring), |
| help='Extra target args to pass to ninja', |
| default=[]), |
| 'board': |
| Property(kind=str, help='Board to build', default=None), |
| 'product': |
| Property(kind=str, help='Product to build', default=None), |
| 'test_pool': |
| Property( |
| kind=str, |
| help='Swarming pool from which a test task will be drawn', |
| default='fuchsia.tests'), |
| 'device_type': |
| Property( |
| kind=str, |
| help='The type of device to execute tests on, if the value is' |
| ' not QEMU it will be passed to Swarming as the device_type' |
| ' dimension', |
| default='QEMU'), |
| 'pave': |
| Property( |
| kind=bool, |
| help='Whether to pave images the device for testing. (Ignored if' |
| ' device_type == QEMU)', |
| default=True), |
| 'test_timeout_secs': |
| Property( |
| kind=Single((int, float)), |
| help='How long to wait until timing out on tests', |
| default=40 * 60), |
| # This property is not intended to be set by a config. It is just here |
| # so that a test case can pass a smaller number in order to reduce the |
| # size of the generated test expectations. |
| 'boots_per_revision': |
| Property( |
| kind=int, |
| help='Number of boots of Fuchsia to run performance tests on,' |
| ' in order to deal with cross-boot variation of performance', |
| default=5), |
| 'debug_symbol_gcs_bucket': |
| Property( |
| kind=str, |
| help='GCS bucket to upload to and read debug symbols from'), |
| 'artifact_gcs_bucket': |
| Property( |
| kind=str, |
| help='GCS bucket to upload to and read build artifacts from'), |
| 'test_task_service_account': |
| Property( |
| kind=str, |
| help='The service account to run test tasks with', |
| default=''), |
| } |
| |
| |
| def git_rev_parse(api, desc, git_dir, rev): |
| with api.context(cwd=git_dir): |
| result = api.step( |
| desc, ['git', 'rev-parse', rev], stdout=api.raw_io.output()) |
| return result.stdout.strip() |
| |
| |
| def git_checkout(api, git_dir, rev): |
| with api.context(cwd=git_dir): |
| api.step('git checkout', ['git', 'checkout', rev]) |
| |
| |
| # Run "git log" on the repo of the change being tested, to list the most |
| # recent commits there. This is to help developers debug the bot run and |
| # understand what it is testing. The change will often be rebased onto |
| # tip-of-tree; the step records what the change was rebased onto. |
| def git_log(api, git_dir): |
| with api.context(cwd=git_dir): |
| api.step('git log for debugging', ['git', 'log', '--max-count=10']) |
| |
| |
| def RunSteps(api, manifest, remote, target, build_type, packages, variants, |
| gn_args, ninja_targets, test_pool, device_type, pave, board, |
| product, test_timeout_secs, boots_per_revision, |
| debug_symbol_gcs_bucket, artifact_gcs_bucket, |
| test_task_service_account): |
| checkout_root = api.path['start_dir'].join('fuchsia') |
| checkout = api.checkout.fuchsia_with_options( |
| path=checkout_root, |
| build=api.buildbucket.build, |
| manifest=manifest, |
| remote=remote, |
| ) |
| |
| snapshot_target = 'obj/build/images/system.snapshot' |
| ninja_targets = list(ninja_targets) |
| ninja_targets.append(snapshot_target) |
| |
| test_cmds = [ |
| ' '.join([ |
| '/pkgfs/packages/fuchsia_benchmarks/0/bin/benchmarks_perfcompare.sh', |
| api.testing_requests.results_dir_on_target |
| ]) |
| ] |
| |
| # This builds Fuchsia at the given revision and kicks off a swarming task |
| # to run the tests. It returns a function that, when called, will wait |
| # for the swarming job to complete and return the test results. This |
| # division into two parts allows swarming tasks to run concurrently. |
| def test_version(before_or_after, git_rev): |
| with api.step.nest('build and launch tests for "%s" revision' % |
| before_or_after): |
| git_checkout(api, checkout.root_dir, git_rev) |
| build_dir = checkout.root_dir.join('out') |
| build = api.build.with_options( |
| build_dir=build_dir, |
| checkout=checkout, |
| target=target, |
| build_type=build_type, |
| packages=packages, |
| variants=variants, |
| gn_args=gn_args, |
| ninja_targets=ninja_targets, |
| board=board, |
| product=product, |
| pave=pave, |
| ) |
| if debug_symbol_gcs_bucket: |
| build.upload_debug_symbols( |
| debug_symbol_gcs_bucket=debug_symbol_gcs_bucket) |
| # Make a copy of the binary size data. |
| snapshot_file = build.fuchsia_build_dir.join(snapshot_target) |
| copied_file = checkout.root_dir.join('snapshot_%s' % before_or_after) |
| api.step('copy binary size data', ['cp', snapshot_file, copied_file]) |
| # Copy the results data into the logs. |
| api.step('binary size data', ['cat', snapshot_file]) |
| |
| # Must be set before testing.shard_requests() is called. |
| api.artifacts.gcs_bucket = artifact_gcs_bucket |
| # The uuid is used for the namespace so append before_or_after to it to |
| # use different namespaces for the before artifacts and after artifacts. |
| api.artifacts.uuid = '%s/%s' % (api.buildbucket_util.id, before_or_after) |
| shard_requests = api.testing_requests.deprecated_shard_requests( |
| build, |
| test_cmds, |
| device_type, |
| test_pool, |
| test_timeout_secs, |
| pave, |
| default_service_account=test_task_service_account, |
| ) |
| # Must be done after testing.shard_requests() is called, because that |
| # modifies the filesystem images. TODO(garymm,joshuaseaton): once legacy_qemu |
| # code paths are removed, remove this comment as it will become false. |
| api.artifacts.upload('upload artifacts', build) |
| |
| orchestration_inputs = api.build.TestOrchestrationInputs.from_build_results( |
| build, shard_requests) |
| collect_funcs = [] |
| for boot_idx in xrange(boots_per_revision): |
| with api.step.nest('boot %d of %d' % |
| (boot_idx + 1, boots_per_revision)): |
| collect_funcs.append( |
| api.testing.deprecated_test_async( |
| debug_symbol_gcs_bucket, |
| device_type, |
| orchestration_inputs, |
| overwrite_summary=False)) |
| |
| def finish_func(): |
| with api.step.nest('collect results for "%s" revision' % before_or_after): |
| results_dir = checkout.root_dir.join('perf_results_%s' % |
| before_or_after) |
| by_boot_dir = results_dir.join('by_boot') |
| api.step('make results directory', ['mkdir', '-p', by_boot_dir]) |
| for boot_idx, collect_func in enumerate(collect_funcs): |
| with api.step.nest('boot %d of %d' % |
| (boot_idx + 1, boots_per_revision)): |
| test_results = collect_func() |
| # Make a copy of the perf test results directory. |
| api.step('copy perf test results', [ |
| 'cp', '-r', test_results.results_dir, |
| by_boot_dir.join('boot%06d' % boot_idx) |
| ]) |
| |
| return { |
| 'binary_size_data_file': copied_file, |
| 'perf_test_results_dir': results_dir |
| } |
| |
| return finish_func |
| |
| rev_before = git_rev_parse(api, 'get "before" revision', checkout.root_dir, |
| 'HEAD^') |
| rev_after = git_rev_parse(api, 'get "after" revision', checkout.root_dir, |
| 'HEAD') |
| git_log(api, checkout.root_dir) |
| # Build the "before" and "after" revisions and launch swarming tasks to |
| # test them. Building will happen sequentially, but the swarming tasks |
| # will run concurrently. |
| finish_before = test_version('before', rev_before) |
| finish_after = test_version('after', rev_after) |
| results_before = finish_before() |
| results_after = finish_after() |
| # Runs with the checkout in the "after" state. |
| perfcompare_tool = checkout.root_dir.join( |
| 'garnet/bin/perfcompare/perfcompare.py') |
| api.python('compare binary sizes before and after', perfcompare_tool, [ |
| 'compare_sizes', results_before['binary_size_data_file'], |
| results_after['binary_size_data_file'] |
| ]) |
| # Output the perf results dataset as a single file so that it can be |
| # easily downloaded. |
| api.python('generate raw_perf_dataset.json', perfcompare_tool, [ |
| 'make_combined_perf_dataset_file', |
| results_before['perf_test_results_dir'], |
| results_after['perf_test_results_dir'] |
| ]) |
| api.python('compare perf test results before and after', perfcompare_tool, [ |
| 'compare_perf', results_before['perf_test_results_dir'], |
| results_after['perf_test_results_dir'] |
| ]) |
| |
| |
| def GenTests(api): |
| # Since the recipe above runs Fuchsia twice (for the "before" and "after" |
| # builds), adjust the dummy output to contain two sets of results. |
| def testing_steps(): |
| steps = [] |
| task_result_step = api.testing.task_step_data( |
| [ |
| api.fuchsia.m.swarming.task_result( |
| # Include serial.txt to cover the processing of the serial log, |
| # since that codepath is used in production and has broken in |
| # the past. |
| id='1', |
| name='test', |
| outputs=['output.fs', 'serial.txt']), |
| ], |
| enable_retries=False, |
| ) |
| steps.append(task_result_step) |
| for rev in ('before', 'after'): |
| for boot in (1, 2): |
| base_name = 'collect results for "%s" revision.boot %d of 2' % (rev, |
| boot) |
| task_result_step.step_data[ |
| base_name + '.collect'] = task_result_step.step_data['collect'] |
| # Include summary.json in the mocked contents of the output archive to |
| # get better coverage of codepaths in the testing recipe module used by |
| # this recipe. |
| summary_data = { |
| 'tests': [{ |
| 'name': 'perfcompare_benchmark.catapult_json', |
| 'result': 'SUCCESS', |
| }] |
| } |
| steps.extend([ |
| api.step_data( |
| base_name + '.get extracted files', |
| api.file.listdir(['summary.json']), |
| ), |
| api.step_data( |
| base_name + '.all test results.read summary.json', |
| api.file.read_text(api.json.dumps(summary_data),), |
| ) |
| ]) |
| del task_result_step.step_data['collect'] |
| return steps |
| |
| yield api.fuchsia.test( |
| 'successful_run', |
| # Pass a smaller value than the default for boots_per_revision to |
| # reduce the size of the test expectations output, but use a number |
| # >1 in order to test multiple boots. |
| properties=dict( |
| run_tests=True, |
| boots_per_revision=2, |
| debug_symbol_gcs_bucket='debug-symbols', |
| artifact_gcs_bucket='fuchsia-infra-artifacts', |
| test_task_service_account='service_account', |
| ), |
| clear_default_steps=True, |
| steps=testing_steps(), |
| ) |