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