• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright (C) 2023 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import sys
17if __name__ == "__main__":
18    sys.dont_write_bytecode = True
19
20import argparse
21import dataclasses
22import datetime
23import json
24import os
25import pathlib
26import random
27import re
28import shutil
29import subprocess
30import time
31import uuid
32from typing import Optional
33
34import pretty
35import utils
36
37
38class FatalError(Exception):
39    def __init__(self):
40        pass
41
42
43class OptionsError(Exception):
44    def __init__(self, message):
45        self.message = message
46
47
48@dataclasses.dataclass(frozen=True)
49class Lunch:
50    "Lunch combination"
51
52    target_product: str
53    "TARGET_PRODUCT"
54
55    target_release: str
56    "TARGET_RELEASE"
57
58    target_build_variant: str
59    "TARGET_BUILD_VARIANT"
60
61    def ToDict(self):
62        return {
63            "TARGET_PRODUCT": self.target_product,
64            "TARGET_RELEASE": self.target_release,
65            "TARGET_BUILD_VARIANT": self.target_build_variant,
66        }
67
68    def Combine(self):
69        return f"{self.target_product}-{self.target_release}-{self.target_build_variant}"
70
71
72@dataclasses.dataclass(frozen=True)
73class Change:
74    "A change that we make to the tree, and how to undo it"
75    label: str
76    "String to print in the log when the change is made"
77
78    change: callable
79    "Function to change the source tree"
80
81    undo: callable
82    "Function to revert the source tree to its previous condition in the most minimal way possible."
83
84_DUMPVARS_VARS=[
85    "COMMON_LUNCH_CHOICES",
86    "HOST_PREBUILT_TAG",
87    "print",
88    "PRODUCT_OUT",
89    "report_config",
90    "TARGET_ARCH",
91    "TARGET_BUILD_VARIANT",
92    "TARGET_DEVICE",
93    "TARGET_PRODUCT",
94]
95
96_DUMPVARS_ABS_VARS =[
97    "ANDROID_CLANG_PREBUILTS",
98    "ANDROID_JAVA_HOME",
99    "ANDROID_JAVA_TOOLCHAIN",
100    "ANDROID_PREBUILTS",
101    "HOST_OUT",
102    "HOST_OUT_EXECUTABLES",
103    "HOST_OUT_TESTCASES",
104    "OUT_DIR",
105    "print",
106    "PRODUCT_OUT",
107    "SOONG_HOST_OUT",
108    "SOONG_HOST_OUT_EXECUTABLES",
109    "TARGET_OUT_TESTCASES",
110]
111
112@dataclasses.dataclass(frozen=True)
113class Benchmark:
114    "Something we measure"
115
116    id: str
117    "Short ID for the benchmark, for the command line"
118
119    title: str
120    "Title for reports"
121
122    change: Change
123    "Source tree modification for the benchmark that will be measured"
124
125    dumpvars: Optional[bool] = False
126    "If specified, soong will run in dumpvars mode rather than build-mode."
127
128    modules: Optional[list[str]] = None
129    "Build modules to build on soong command line"
130
131    preroll: Optional[int] = 0
132    "Number of times to run the build command to stabilize"
133
134    postroll: Optional[int] = 3
135    "Number of times to run the build command after reverting the action to stabilize"
136
137    def build_description(self):
138      "Short description of the benchmark's Soong invocation."
139      if self.dumpvars:
140        return "dumpvars"
141      elif self.modules:
142        return " ".join(self.modules)
143      return ""
144
145
146    def soong_command(self, root):
147      "Command line args to soong_ui for this benchmark."
148      if self.dumpvars:
149          return [
150              "--dumpvars-mode",
151              f"--vars=\"{' '.join(_DUMPVARS_VARS)}\"",
152              f"--abs-vars=\"{' '.join(_DUMPVARS_ABS_VARS)}\"",
153              "--var-prefix=var_cache_",
154              "--abs-var-prefix=abs_var_cache_",
155          ]
156      elif self.modules:
157          return [
158              "--build-mode",
159              "--all-modules",
160              f"--dir={root}",
161              "--skip-metrics-upload",
162          ] + self.modules
163      else:
164          raise Exception("Benchmark must specify dumpvars or modules")
165
166
167@dataclasses.dataclass(frozen=True)
168class FileSnapshot:
169    "Snapshot of a file's contents."
170
171    filename: str
172    "The file that was snapshottened"
173
174    contents: str
175    "The contents of the file"
176
177    def write(self):
178        "Write the contents back to the file"
179        with open(self.filename, "w") as f:
180            f.write(self.contents)
181
182
183def Snapshot(filename):
184    """Return a FileSnapshot with the file's current contents."""
185    with open(filename) as f:
186        contents = f.read()
187    return FileSnapshot(filename, contents)
188
189
190def Clean():
191    """Remove the out directory."""
192    def remove_out():
193        out_dir = utils.get_out_dir()
194        #only remove actual contents, in case out is a symlink (as is the case for cog)
195        if os.path.exists(out_dir):
196          for filename in os.listdir(out_dir):
197              p = os.path.join(out_dir, filename)
198              if os.path.isfile(p) or os.path.islink(p):
199                  os.remove(p)
200              elif os.path.isdir(p):
201                  shutil.rmtree(p)
202    return Change(label="Remove out", change=remove_out, undo=lambda: None)
203
204
205def CleanNinja():
206    """Remove the out directory, and then run lunch to initialize soong"""
207    def clean_ninja():
208        returncode = subprocess.call("rm out/*.ninja out/soong/*.ninja", shell=True)
209        if returncode != 0:
210            report_error(f"Build failed: {' '.join(cmd)}")
211            raise FatalError()
212    return Change(label="Remove ninja files", change=clean_ninja, undo=lambda: None)
213
214
215def NoChange():
216    """No change to the source tree."""
217    return Change(label="No change", change=lambda: None, undo=lambda: None)
218
219
220def Create(filename):
221    "Create an action to create `filename`. The parent directory must exist."
222    def create():
223        with open(filename, "w") as f:
224            pass
225    def delete():
226        os.remove(filename)
227    return Change(
228                label=f"Create {filename}",
229                change=create,
230                undo=delete,
231            )
232
233
234def Modify(filename, contents, before=None):
235    """Create an action to modify `filename` by appending the result of `contents`
236    before the last instances of `before` in the file.
237
238    Raises an error if `before` doesn't appear in the file.
239    """
240    orig = Snapshot(filename)
241    if before:
242        index = orig.contents.rfind(before)
243        if index < 0:
244            report_error(f"{filename}: Unable to find string '{before}' for modify operation.")
245            raise FatalError()
246    else:
247        index = len(orig.contents)
248    modified = FileSnapshot(filename, orig.contents[:index] + contents() + orig.contents[index:])
249    if False:
250        print(f"Modify: {filename}")
251        x = orig.contents.replace("\n", "\n   ORIG")
252        print(f"   ORIG {x}")
253        x = modified.contents.replace("\n", "\n   MODIFIED")
254        print(f"   MODIFIED {x}")
255
256    return Change(
257            label="Modify " + filename,
258            change=lambda: modified.write(),
259            undo=lambda: orig.write()
260        )
261
262def ChangePublicApi():
263    change = AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
264                 "@android.annotation.SuppressLint(\"UnflaggedApi\") public")
265    orig_current_text = Snapshot("frameworks/base/core/api/current.txt")
266
267    def undo():
268        change.undo()
269        orig_current_text.write()
270
271    return Change(
272        label=change.label,
273        change=change.change,
274        undo=lambda: undo()
275    )
276
277def AddJavaField(filename, prefix):
278    return Modify(filename,
279                  lambda: f"{prefix} static final int BENCHMARK = {random.randint(0, 1000000)};\n",
280                  before="}")
281
282
283def Comment(prefix, suffix=""):
284    return lambda: prefix + " " + str(uuid.uuid4()) + suffix
285
286
287class BenchmarkReport():
288    "Information about a run of the benchmark"
289
290    lunch: Lunch
291    "lunch combo"
292
293    benchmark: Benchmark
294    "The benchmark object."
295
296    iteration: int
297    "Which iteration of the benchmark"
298
299    log_dir: str
300    "Path the the log directory, relative to the root of the reports directory"
301
302    preroll_duration_ns: [int]
303    "Durations of the in nanoseconds."
304
305    duration_ns: int
306    "Duration of the measured portion of the benchmark in nanoseconds."
307
308    postroll_duration_ns: [int]
309    "Durations of the postrolls in nanoseconds."
310
311    complete: bool
312    "Whether the benchmark made it all the way through the postrolls."
313
314    def __init__(self, lunch, benchmark, iteration, log_dir):
315        self.lunch = lunch
316        self.benchmark = benchmark
317        self.iteration = iteration
318        self.log_dir = log_dir
319        self.preroll_duration_ns = []
320        self.duration_ns = -1
321        self.postroll_duration_ns = []
322        self.complete = False
323
324    def ToDict(self):
325        return {
326            "lunch": self.lunch.ToDict(),
327            "id": self.benchmark.id,
328            "title": self.benchmark.title,
329            "modules": self.benchmark.modules,
330            "dumpvars": self.benchmark.dumpvars,
331            "change": self.benchmark.change.label,
332            "iteration": self.iteration,
333            "log_dir": self.log_dir,
334            "preroll_duration_ns": self.preroll_duration_ns,
335            "duration_ns": self.duration_ns,
336            "postroll_duration_ns": self.postroll_duration_ns,
337            "complete": self.complete,
338        }
339
340class Runner():
341    """Runs the benchmarks."""
342
343    def __init__(self, options):
344        self._options = options
345        self._reports = []
346        self._complete = False
347
348    def Run(self):
349        """Run all of the user-selected benchmarks."""
350
351        # With `--list`, just list the benchmarks available.
352        if self._options.List():
353            print(" ".join(self._options.BenchmarkIds()))
354            return
355
356        # Clean out the log dir or create it if necessary
357        prepare_log_dir(self._options.LogDir())
358
359        try:
360            for lunch in self._options.Lunches():
361                print(lunch)
362                for benchmark in self._options.Benchmarks():
363                    for iteration in range(self._options.Iterations()):
364                        self._run_benchmark(lunch, benchmark, iteration)
365            self._complete = True
366        finally:
367            self._write_summary()
368
369
370    def _run_benchmark(self, lunch, benchmark, iteration):
371        """Run a single benchmark."""
372        benchmark_log_subdir = self._benchmark_log_dir(lunch, benchmark, iteration)
373        benchmark_log_dir = self._options.LogDir().joinpath(benchmark_log_subdir)
374
375        sys.stderr.write(f"STARTING BENCHMARK: {benchmark.id}\n")
376        sys.stderr.write(f"             lunch: {lunch.Combine()}\n")
377        sys.stderr.write(f"         iteration: {iteration}\n")
378        sys.stderr.write(f" benchmark_log_dir: {benchmark_log_dir}\n")
379
380        report = BenchmarkReport(lunch, benchmark, iteration, benchmark_log_subdir)
381        self._reports.append(report)
382
383        # Preroll builds
384        for i in range(benchmark.preroll):
385            ns = self._run_build(lunch, benchmark_log_dir.joinpath(f"pre_{i}"), benchmark)
386            report.preroll_duration_ns.append(ns)
387
388        sys.stderr.write(f"PERFORMING CHANGE: {benchmark.change.label}\n")
389        if not self._options.DryRun():
390            benchmark.change.change()
391        try:
392
393            # Measured build
394            ns = self._run_build(lunch, benchmark_log_dir.joinpath("measured"), benchmark)
395            report.duration_ns = ns
396
397            dist_one = self._options.DistOne()
398            if dist_one:
399                # If we're disting just one benchmark, save the logs and we can stop here.
400                self._dist(utils.get_dist_dir(), benchmark.dumpvars)
401            else:
402                self._dist(benchmark_log_dir, benchmark.dumpvars, store_metrics_only=True)
403                # Postroll builds
404                for i in range(benchmark.postroll):
405                    ns = self._run_build(lunch, benchmark_log_dir.joinpath(f"post_{i}"),
406                                         benchmark)
407                    report.postroll_duration_ns.append(ns)
408
409        finally:
410            # Always undo, even if we crashed or the build failed and we stopped.
411            sys.stderr.write(f"UNDOING CHANGE: {benchmark.change.label}\n")
412            if not self._options.DryRun():
413                benchmark.change.undo()
414
415        self._write_summary()
416        sys.stderr.write(f"FINISHED BENCHMARK: {benchmark.id}\n")
417
418    def _benchmark_log_dir(self, lunch, benchmark, iteration):
419        """Construct the log directory fir a benchmark run."""
420        path = f"{lunch.Combine()}/{benchmark.id}"
421        # Zero pad to the correct length for correct alpha sorting
422        path += ("/%0" + str(len(str(self._options.Iterations()))) + "d") % iteration
423        return path
424
425    def _run_build(self, lunch, build_log_dir, benchmark):
426        """Builds the modules.  Saves interesting log files to log_dir.  Raises FatalError
427        if the build fails.
428        """
429        sys.stderr.write(f"STARTING BUILD {benchmark.build_description()} Logs to: {build_log_dir}\n")
430
431        before_ns = time.perf_counter_ns()
432        if not self._options.DryRun():
433            cmd = [
434                "build/soong/soong_ui.bash",
435            ] + benchmark.soong_command(self._options.root)
436            env = dict(os.environ)
437            env["TARGET_PRODUCT"] = lunch.target_product
438            env["TARGET_RELEASE"] = lunch.target_release
439            env["TARGET_BUILD_VARIANT"] = lunch.target_build_variant
440            returncode = subprocess.call(cmd, env=env)
441            if returncode != 0:
442                report_error(f"Build failed: {' '.join(cmd)}")
443                raise FatalError()
444
445        after_ns = time.perf_counter_ns()
446
447        # TODO: Copy some log files.
448
449        sys.stderr.write(f"FINISHED BUILD {benchmark.build_description()}\n")
450
451        return after_ns - before_ns
452
453    def _dist(self, dist_dir, dumpvars, store_metrics_only=False):
454        out_dir = utils.get_out_dir()
455        dest_dir = dist_dir.joinpath("logs")
456        os.makedirs(dest_dir, exist_ok=True)
457        basenames = [
458            "soong_build_metrics.pb",
459            "soong_metrics",
460        ]
461        if not store_metrics_only:
462            basenames.extend([
463                "build.trace.gz",
464                "soong.log",
465            ])
466        if dumpvars:
467            basenames = ['dumpvars-'+b for b in basenames]
468        for base in basenames:
469            src = out_dir.joinpath(base)
470            if src.exists():
471                sys.stderr.write(f"DIST: copied {src} to {dest_dir}\n")
472                shutil.copy(src, dest_dir)
473
474    def _write_summary(self):
475        # Write the results, even if the build failed or we crashed, including
476        # whether we finished all of the benchmarks.
477        data = {
478            "start_time": self._options.Timestamp().isoformat(),
479            "branch": self._options.Branch(),
480            "tag": self._options.Tag(),
481            "benchmarks": [report.ToDict() for report in self._reports],
482            "complete": self._complete,
483        }
484        with open(self._options.LogDir().joinpath("summary.json"), "w", encoding="utf-8") as f:
485            json.dump(data, f, indent=2, sort_keys=True)
486
487
488def benchmark_table(benchmarks):
489    rows = [("ID", "DESCRIPTION", "REBUILD"),]
490    rows += [(benchmark.id, benchmark.title, benchmark.build_description()) for benchmark in
491             benchmarks]
492    return rows
493
494
495def prepare_log_dir(directory):
496    if os.path.exists(directory):
497        # If it exists and isn't a directory, fail.
498        if not os.path.isdir(directory):
499            report_error(f"Log directory already exists but isn't a directory: {directory}")
500            raise FatalError()
501        # Make sure the directory is empty. Do this rather than deleting it to handle
502        # symlinks cleanly.
503        for filename in os.listdir(directory):
504            entry = os.path.join(directory, filename)
505            if os.path.isdir(entry):
506                shutil.rmtree(entry)
507            else:
508                os.unlink(entry)
509    else:
510        # Create it
511        os.makedirs(directory)
512
513
514class Options():
515    def __init__(self):
516        self._had_error = False
517
518        # Wall time clock when we started
519        self._timestamp = datetime.datetime.now(datetime.timezone.utc)
520
521        # Move to the root of the tree right away. Everything must happen from there.
522        self.root = utils.get_root()
523        if not self.root:
524            report_error("Unable to find root of tree from cwd.")
525            raise FatalError()
526        os.chdir(self.root)
527
528        # Initialize the Benchmarks. Note that this pre-loads all of the files, etc.
529        # Doing all that here forces us to fail fast if one of them can't load a required
530        # file, at the cost of a small startup speed. Don't make this do something slow
531        # like scan the whole tree.
532        self._init_benchmarks()
533
534        # Argument parsing
535        epilog = f"""
536benchmarks:
537{pretty.FormatTable(benchmark_table(self._benchmarks), prefix="  ")}
538"""
539
540        parser = argparse.ArgumentParser(
541                prog="benchmarks",
542                allow_abbrev=False, # Don't let people write unsupportable scripts.
543                formatter_class=argparse.RawDescriptionHelpFormatter,
544                epilog=epilog,
545                description="Run build system performance benchmarks.")
546        self.parser = parser
547
548        parser.add_argument("--log-dir",
549                            help="Directory for logs. Default is $TOP/../benchmarks/.")
550        parser.add_argument("--dated-logs", action="store_true",
551                            help="Append timestamp to log dir.")
552        parser.add_argument("-n", action="store_true", dest="dry_run",
553                            help="Dry run. Don't run the build commands but do everything else.")
554        parser.add_argument("--tag",
555                            help="Variant of the run, for when there are multiple perf runs.")
556        parser.add_argument("--lunch", nargs="*",
557                            help="Lunch combos to test")
558        parser.add_argument("--iterations", type=int, default=1,
559                            help="Number of iterations of each test to run.")
560        parser.add_argument("--branch", type=str,
561                            help="Specify branch. Otherwise a guess will be made based on repo.")
562        parser.add_argument("--benchmark", nargs="*", default=[b.id for b in self._benchmarks],
563                            metavar="BENCHMARKS",
564                            help="Benchmarks to run.  Default suite will be run if omitted.")
565        parser.add_argument("--list", action="store_true",
566                            help="list the available benchmarks.  No benchmark is run.")
567        parser.add_argument("--dist-one", action="store_true",
568                            help="Copy logs and metrics to the given dist dir. Requires that only"
569                                + " one benchmark be supplied. Postroll steps will be skipped.")
570
571        self._args = parser.parse_args()
572
573        self._branch = self._branch()
574        self._log_dir = self._log_dir()
575        self._lunches = self._lunches()
576
577        # Validate the benchmark ids
578        all_ids = [benchmark.id for benchmark in self._benchmarks]
579        bad_ids = [id for id in self._args.benchmark if id not in all_ids]
580        if bad_ids:
581            for id in bad_ids:
582                self._error(f"Invalid benchmark: {id}")
583
584        # --dist-one requires that only one benchmark be supplied
585        if self._args.dist_one and len(self.Benchmarks()) != 1:
586            self._error("--dist-one requires exactly one --benchmark.")
587
588        if self._had_error:
589            raise FatalError()
590
591    def Timestamp(self):
592        return self._timestamp
593
594    def _branch(self):
595        """Return the branch, either from the command line or by guessing from repo."""
596        if self._args.branch:
597            return self._args.branch
598        try:
599            branch = subprocess.check_output(f"cd {self.root}/.repo/manifests"
600                        + " && git rev-parse --abbrev-ref --symbolic-full-name @{u}",
601                    shell=True, encoding="utf-8")
602            return branch.strip().split("/")[-1]
603        except subprocess.CalledProcessError as ex:
604            report_error("Can't get branch from .repo dir. Specify --branch argument")
605            report_error(str(ex))
606            raise FatalError()
607
608    def Branch(self):
609        return self._branch
610
611    def _log_dir(self):
612        "The log directory to use, based on the current options"
613        if self._args.log_dir:
614            d = pathlib.Path(self._args.log_dir).resolve().absolute()
615        else:
616            d = self.root.joinpath("..", utils.DEFAULT_REPORT_DIR)
617        if self._args.dated_logs:
618            d = d.joinpath(self._timestamp.strftime('%Y-%m-%d'))
619        d = d.joinpath(self._branch)
620        if self._args.tag:
621            d = d.joinpath(self._args.tag)
622        return d.resolve().absolute()
623
624    def LogDir(self):
625        return self._log_dir
626
627    def Benchmarks(self):
628        return [b for b in self._benchmarks if b.id in self._args.benchmark]
629
630    def Tag(self):
631        return self._args.tag
632
633    def DryRun(self):
634        return self._args.dry_run
635
636    def List(self):
637        return self._args.list
638
639    def BenchmarkIds(self) :
640        return [benchmark.id for benchmark in self._benchmarks]
641
642    def _lunches(self):
643        def parse_lunch(lunch):
644            parts = lunch.split("-")
645            if len(parts) != 3:
646                raise OptionsError(f"Invalid lunch combo: {lunch}")
647            return Lunch(parts[0], parts[1], parts[2])
648        # If they gave lunch targets on the command line use that
649        if self._args.lunch:
650            result = []
651            # Split into Lunch objects
652            for lunch in self._args.lunch:
653                try:
654                    result.append(parse_lunch(lunch))
655                except OptionsError as ex:
656                    self._error(ex.message)
657            return result
658        # Use whats in the environment
659        product = os.getenv("TARGET_PRODUCT")
660        release = os.getenv("TARGET_RELEASE")
661        variant = os.getenv("TARGET_BUILD_VARIANT")
662        if (not product) or (not release) or (not variant):
663            # If they didn't give us anything, fail rather than guessing. There's no good
664            # default for AOSP.
665            self._error("No lunch combo specified. Either pass --lunch argument or run lunch.")
666            return []
667        return [Lunch(product, release, variant),]
668
669    def Lunches(self):
670        return self._lunches
671
672    def Iterations(self):
673        return self._args.iterations
674
675    def DistOne(self):
676        return self._args.dist_one
677
678    def _init_benchmarks(self):
679        """Initialize the list of benchmarks."""
680        # Assumes that we've already chdired to the root of the tree.
681        self._benchmarks = [
682            Benchmark(
683                      id="full_lunch",
684                      title="Lunch from clean out",
685                      change=Clean(),
686                      dumpvars=True,
687                      preroll=0,
688                      postroll=0,
689            ),
690            Benchmark(
691                      id="noop_lunch",
692                      title="Lunch with no change",
693                      change=NoChange(),
694                      dumpvars=True,
695                      preroll=1,
696                      postroll=0,
697            ),
698            Benchmark(id="full",
699                      title="Full build",
700                      change=Clean(),
701                      modules=["droid"],
702                      preroll=0,
703                      postroll=3,
704                      ),
705            Benchmark(id="nochange",
706                      title="No change",
707                      change=NoChange(),
708                      modules=["droid"],
709                      preroll=2,
710                      postroll=3,
711                      ),
712            Benchmark(id="unreferenced",
713                      title="Create unreferenced file",
714                      change=Create("bionic/unreferenced.txt"),
715                      modules=["droid"],
716                      preroll=1,
717                      postroll=2,
718                      ),
719            Benchmark(id="modify_bp",
720                      title="Modify Android.bp",
721                      change=Modify("bionic/libc/Android.bp", Comment("//")),
722                      modules=["droid"],
723                      preroll=1,
724                      postroll=3,
725                      ),
726            Benchmark(id="full_analysis",
727                      title="Full Analysis",
728                      change=CleanNinja(),
729                      modules=["nothing"],
730                      preroll=1,
731                      postroll=3,
732                      ),
733            Benchmark(id="modify_stdio",
734                      title="Modify stdio.cpp",
735                      change=Modify("bionic/libc/stdio/stdio.cpp", Comment("//")),
736                      modules=["libc"],
737                      preroll=1,
738                      postroll=2,
739                      ),
740            Benchmark(id="modify_adbd",
741                      title="Modify adbd",
742                      change=Modify("packages/modules/adb/daemon/main.cpp", Comment("//")),
743                      modules=["adbd"],
744                      preroll=1,
745                      postroll=2,
746                      ),
747            Benchmark(id="services_private_field",
748                      title="Add private field to ActivityManagerService.java",
749                      change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
750                                          "private"),
751                      modules=["services"],
752                      preroll=1,
753                      postroll=2,
754                      ),
755            Benchmark(id="services_public_field",
756                      title="Add public field to ActivityManagerService.java",
757                      change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
758                                          "/** @hide */ public"),
759                      modules=["services"],
760                      preroll=1,
761                      postroll=2,
762                      ),
763            Benchmark(id="services_api",
764                      title="Add API to ActivityManagerService.javaa",
765                      change=AddJavaField("frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java",
766                                          "@android.annotation.SuppressLint(\"UnflaggedApi\") public"),
767                      modules=["services"],
768                      preroll=1,
769                      postroll=2,
770                      ),
771            Benchmark(id="framework_private_field",
772                      title="Add private field to Settings.java",
773                      change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
774                                          "private"),
775                      modules=["framework-minus-apex"],
776                      preroll=1,
777                      postroll=2,
778                      ),
779            Benchmark(id="framework_public_field",
780                      title="Add public field to Settings.java",
781                      change=AddJavaField("frameworks/base/core/java/android/provider/Settings.java",
782                                          "/** @hide */ public"),
783                      modules=["framework-minus-apex"],
784                      preroll=1,
785                      postroll=2,
786                      ),
787            Benchmark(id="framework_api",
788                      title="Add API to Settings.java",
789                      change=ChangePublicApi(),
790                      modules=["api-stubs-docs-non-updatable-update-current-api", "framework-minus-apex"],
791                      preroll=1,
792                      postroll=2,
793                      ),
794            Benchmark(id="modify_framework_resource",
795                      title="Modify framework resource",
796                      change=Modify("frameworks/base/core/res/res/values/config.xml",
797                                    lambda: str(uuid.uuid4()),
798                                    before="</string>"),
799                      modules=["framework-minus-apex"],
800                      preroll=1,
801                      postroll=2,
802                      ),
803            Benchmark(id="add_framework_resource",
804                      title="Add framework resource",
805                      change=Modify("frameworks/base/core/res/res/values/config.xml",
806                                    lambda: f"<string name=\"BENCHMARK\">{uuid.uuid4()}</string>",
807                                    before="</resources>"),
808                      modules=["framework-minus-apex"],
809                      preroll=1,
810                      postroll=2,
811                      ),
812            Benchmark(id="add_systemui_field",
813                      title="Add SystemUI field",
814                      change=AddJavaField("frameworks/base/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java",
815                                    "public"),
816                      modules=["SystemUI"],
817                      preroll=1,
818                      postroll=2,
819                      ),
820            Benchmark(id="add_systemui_field_with_tests",
821                      title="Add SystemUI field with tests",
822                      change=AddJavaField("frameworks/base/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java",
823                                    "public"),
824                      modules=["SystemUiRavenTests"],
825                      preroll=1,
826                      postroll=2,
827                      ),
828            Benchmark(id="systemui_flicker_add_log_call",
829                      title="Add a Log call to flicker",
830                      change=Modify("platform_testing/libraries/flicker/src/android/tools/flicker/FlickerServiceResultsCollector.kt",
831                                    lambda: f'Log.v(LOG_TAG, "BENCHMARK = {random.randint(0, 1000000)}");\n',
832                                    before="Log.v(LOG_TAG,"),
833                      modules=["WMShellFlickerTestsPip"],
834                      preroll=1,
835                      postroll=2,
836                      ),
837            Benchmark(id="systemui_core_add_log_call",
838                      title="Add a Log call SystemUIApplication",
839                      change=Modify("frameworks/base/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java",
840                                    lambda: f'Log.v(TAG, "BENCHMARK = {random.randint(0, 1000000)}");\n',
841                                    before="Log.wtf(TAG,"),
842                      modules=["SystemUI-core"],
843                      preroll=1,
844                      postroll=2,
845                      ),
846        ]
847
848    def _error(self, message):
849        report_error(message)
850        self._had_error = True
851
852
853def report_error(message):
854    sys.stderr.write(f"error: {message}\n")
855
856
857def main(argv):
858    try:
859        options = Options()
860        runner = Runner(options)
861        runner.Run()
862    except FatalError:
863        sys.stderr.write(f"FAILED\n")
864        sys.exit(1)
865
866
867if __name__ == "__main__":
868    main(sys.argv)
869