1#!/usr/bin/env python 2# This Source Code Form is subject to the terms of the Mozilla Public 3# License, v. 2.0. If a copy of the MPL was not distributed with this 4# file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 6from __future__ import absolute_import, division, print_function 7 8import six 9import os 10import sys 11import json 12import math 13import glob 14 15AWSY_PATH = os.path.dirname(os.path.realpath(__file__)) 16if AWSY_PATH not in sys.path: 17 sys.path.append(AWSY_PATH) 18 19import parse_about_memory 20 21# A description of each checkpoint and the root path to it. 22CHECKPOINTS = [ 23 {"name": "Fresh start", "path": "memory-report-Start-0.json.gz"}, 24 {"name": "Fresh start [+30s]", "path": "memory-report-StartSettled-0.json.gz"}, 25 {"name": "After tabs open", "path": "memory-report-TabsOpen-4.json.gz"}, 26 { 27 "name": "After tabs open [+30s]", 28 "path": "memory-report-TabsOpenSettled-4.json.gz", 29 }, 30 { 31 "name": "After tabs open [+30s, forced GC]", 32 "path": "memory-report-TabsOpenForceGC-4.json.gz", 33 }, 34 { 35 "name": "Tabs closed extra processes", 36 "path": "memory-report-TabsClosedExtraProcesses-4.json.gz", 37 }, 38 {"name": "Tabs closed", "path": "memory-report-TabsClosed-4.json.gz"}, 39 {"name": "Tabs closed [+30s]", "path": "memory-report-TabsClosedSettled-4.json.gz"}, 40 { 41 "name": "Tabs closed [+30s, forced GC]", 42 "path": "memory-report-TabsClosedForceGC-4.json.gz", 43 }, 44] 45 46# A description of each perfherder suite and the path to its values. 47PERF_SUITES = [ 48 {"name": "Resident Memory", "node": "resident"}, 49 {"name": "Explicit Memory", "node": "explicit/"}, 50 {"name": "Heap Unclassified", "node": "explicit/heap-unclassified"}, 51 {"name": "JS", "node": "js-main-runtime/"}, 52 {"name": "Images", "node": "explicit/images/"}, 53] 54 55 56def median(values): 57 sorted_ = sorted(values) 58 # pylint --py3k W1619 59 med = int(len(sorted_) / 2) 60 61 if len(sorted_) % 2: 62 return sorted_[med] 63 # pylint --py3k W1619 64 return (sorted_[med - 1] + sorted_[med]) / 2 65 66 67def update_checkpoint_paths(checkpoint_files, checkpoints): 68 """ 69 Updates checkpoints with memory report file fetched in data_path 70 :param checkpoint_files: list of files in data_path 71 :param checkpoints: The checkpoints to update the path of. 72 """ 73 target_path = [ 74 ["Start-", 0], 75 ["StartSettled-", 0], 76 ["TabsOpen-", -1], 77 ["TabsOpenSettled-", -1], 78 ["TabsOpenForceGC-", -1], 79 ["TabsClosedExtraProcesses-", -1], 80 ["TabsClosed-", -1], 81 ["TabsClosedSettled-", -1], 82 ["TabsClosedForceGC-", -1], 83 ] 84 for i in range(len(target_path)): 85 (name, idx) = target_path[i] 86 paths = sorted([x for x in checkpoint_files if name in x]) 87 if paths: 88 indices = [i for i, x in enumerate(checkpoints) if name in x["path"]] 89 if indices: 90 checkpoints[indices[0]]["path"] = paths[idx] 91 else: 92 print("found files but couldn't find {}".format(name)) 93 94 95def create_suite( 96 name, node, data_path, checkpoints=CHECKPOINTS, alertThreshold=None, extra_opts=None 97): 98 """ 99 Creates a suite suitable for adding to a perfherder blob. Calculates the 100 geometric mean of the checkpoint values and adds that to the suite as 101 well. 102 103 :param name: The name of the suite. 104 :param node: The path of the data node to extract data from. 105 :param data_path: The directory to retrieve data from. 106 :param checkpoints: Which checkpoints to include. 107 :param alertThreshold: The percentage of change that triggers an alert. 108 """ 109 suite = {"name": name, "subtests": [], "lowerIsBetter": True, "unit": "bytes"} 110 111 if alertThreshold: 112 suite["alertThreshold"] = alertThreshold 113 114 opts = [] 115 if extra_opts: 116 opts.extend(extra_opts) 117 118 # The stylo attributes override each other. 119 stylo_opt = None 120 if "STYLO_FORCE_ENABLED" in os.environ and os.environ["STYLO_FORCE_ENABLED"]: 121 stylo_opt = "stylo" 122 if "STYLO_THREADS" in os.environ and os.environ["STYLO_THREADS"] == "1": 123 stylo_opt = "stylo-sequential" 124 125 if stylo_opt: 126 opts.append(stylo_opt) 127 128 if "DMD" in os.environ and os.environ["DMD"]: 129 opts.append("dmd") 130 131 if extra_opts: 132 suite["extraOptions"] = opts 133 134 update_checkpoint_paths( 135 glob.glob(os.path.join(data_path, "memory-report*")), checkpoints 136 ) 137 138 total = 0 139 for checkpoint in checkpoints: 140 memory_report_path = os.path.join(data_path, checkpoint["path"]) 141 142 name_filter = checkpoint.get("name_filter", None) 143 if checkpoint.get("median"): 144 process = median 145 else: 146 process = sum 147 148 if node != "resident": 149 totals = parse_about_memory.calculate_memory_report_values( 150 memory_report_path, node, name_filter 151 ) 152 value = process(totals.values()) 153 else: 154 # For "resident" we really want RSS of the chrome ("Main") process 155 # and USS of the child processes. We'll still call it resident 156 # for simplicity (it's nice to be able to compare RSS of non-e10s 157 # with RSS + USS of e10s). 158 totals_rss = parse_about_memory.calculate_memory_report_values( 159 memory_report_path, node, ["Main"] 160 ) 161 totals_uss = parse_about_memory.calculate_memory_report_values( 162 memory_report_path, "resident-unique" 163 ) 164 value = list(totals_rss.values())[0] + sum( 165 [v for k, v in six.iteritems(totals_uss) if "Main" not in k] 166 ) 167 168 subtest = { 169 "name": checkpoint["name"], 170 "value": value, 171 "lowerIsBetter": True, 172 "unit": "bytes", 173 } 174 suite["subtests"].append(subtest) 175 total += math.log(subtest["value"]) 176 177 # Add the geometric mean. For more details on the calculation see: 178 # https://en.wikipedia.org/wiki/Geometric_mean#Relationship_with_arithmetic_mean_of_logarithms 179 # pylint --py3k W1619 180 suite["value"] = math.exp(total / len(checkpoints)) 181 182 return suite 183 184 185def create_perf_data( 186 data_path, perf_suites=PERF_SUITES, checkpoints=CHECKPOINTS, extra_opts=None 187): 188 """ 189 Builds up a performance data blob suitable for submitting to perfherder. 190 """ 191 if ("GCOV_PREFIX" in os.environ) or ("JS_CODE_COVERAGE_OUTPUT_DIR" in os.environ): 192 print( 193 "Code coverage is being collected, performance data will not be gathered." 194 ) 195 return {} 196 197 perf_blob = {"framework": {"name": "awsy"}, "suites": []} 198 199 for suite in perf_suites: 200 perf_blob["suites"].append( 201 create_suite( 202 suite["name"], 203 suite["node"], 204 data_path, 205 checkpoints, 206 suite.get("alertThreshold"), 207 extra_opts, 208 ) 209 ) 210 211 return perf_blob 212 213 214if __name__ == "__main__": 215 args = sys.argv[1:] 216 if not args: 217 print("Usage: process_perf_data.py data_path") 218 sys.exit(1) 219 220 # Determine which revisions we need to process. 221 data_path = args[0] 222 perf_blob = create_perf_data(data_path) 223 print("PERFHERDER_DATA: {}".format(json.dumps(perf_blob))) 224 225 sys.exit(0) 226