Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / tools / lint / perfdocs / generator.py
blob240468d62ce60c928de1a3e0b7797eab5e55761f
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 import pathlib
5 import re
6 import shutil
7 import tempfile
9 from perfdocs.logger import PerfDocLogger
10 from perfdocs.utils import (
11 ON_TRY,
12 are_dirs_equal,
13 get_changed_files,
14 read_file,
15 read_yaml,
16 save_file,
19 logger = PerfDocLogger()
22 class Generator(object):
23 """
24 After each perfdocs directory was validated, the generator uses the templates
25 for each framework, fills them with the test descriptions in config and saves
26 the perfdocs in the form index.rst as index file and suite_name.rst for
27 each suite of tests in the framework.
28 """
30 def __init__(self, verifier, workspace, generate=False):
31 """
32 Initialize the Generator.
34 :param verifier: Verifier object. It should not be a fresh Verifier object,
35 but an initialized one with validate_tree() method already called
36 :param workspace: Path to the top-level checkout directory.
37 :param generate: Flag for generating the documentation
38 """
39 self._workspace = workspace
40 if not self._workspace:
41 raise Exception("PerfDocs Generator requires a workspace directory.")
42 # Template documents without added information reside here
43 self.templates_path = pathlib.Path(
44 self._workspace, "tools", "lint", "perfdocs", "templates"
46 self.perfdocs_path = pathlib.Path(
47 self._workspace, "testing", "perfdocs", "generated"
50 self._generate = generate
51 self._verifier = verifier
52 self._perfdocs_tree = self._verifier._gatherer.perfdocs_tree
54 def build_perfdocs_from_tree(self):
55 """
56 Builds up a document for each framework that was found.
58 :return dict: A dictionary containing a mapping from each framework
59 to the document that was built for it, i.e:
61 framework_name: framework_document,
62 ...
64 """
66 # Using the verified `perfdocs_tree`, build up the documentation.
67 frameworks_info = {}
68 for framework in self._perfdocs_tree:
69 yaml_content = read_yaml(pathlib.Path(framework["path"], framework["yml"]))
70 rst_content = read_file(
71 pathlib.Path(framework["path"], framework["rst"]), stringify=True
73 gatherer = self._verifier._gatherer.framework_gatherers[
74 yaml_content["name"]
77 metrics_rst_content = None
78 metrics_info = self._verifier.metrics_info[yaml_content["name"]]
79 if framework.get("metrics"):
80 metrics_rst_content = read_file(
81 pathlib.Path(framework["path"], framework["metrics"]),
82 stringify=True,
85 # Gather all tests and descriptions and format them into
86 # documentation content
87 documentation = []
88 suites = yaml_content["suites"]
89 for suite_name in sorted(suites.keys()):
90 suite_info = suites[suite_name]
92 # Add the suite section
93 documentation.extend(
94 self._verifier._gatherer.framework_gatherers[
95 yaml_content["name"]
96 ].build_suite_section(suite_name, suite_info["description"])
99 tests = suite_info.get("tests", {})
100 for test_name in sorted(tests.keys()):
101 test_description = gatherer.build_test_description(
102 test_name,
103 test_description=tests[test_name],
104 suite_name=suite_name,
105 metrics_info=metrics_info,
107 documentation.extend(test_description)
108 documentation.append("")
110 # For each framework, we may want to organize the metrics differently
111 # so delegate the complete setup of the metric documentation to the
112 # framework-specific gatherers
113 metrics_rst = ""
114 if metrics_rst_content:
115 metrics_documentation = gatherer.build_metrics_documentation(
116 metrics_info
118 metrics_rst = re.sub(
119 r"{metrics_documentation}",
120 "\n".join(metrics_documentation),
121 metrics_rst_content,
123 rst_content = re.sub(
124 r"{metrics_rst_name}",
125 f"{yaml_content['name']}-metrics",
126 rst_content,
129 # Insert documentation into `.rst` file
130 framework_rst = re.sub(
131 r"{documentation}", "\n".join(documentation), rst_content
133 frameworks_info[yaml_content["name"]] = {
134 "dynamic": framework_rst,
135 "metrics": metrics_rst,
136 "static": [],
139 # For static `.rst` file
140 for static_file in framework["static"]:
141 if static_file.endswith("rst"):
142 frameworks_info[yaml_content["name"]]["static"].append(
144 "file": static_file,
145 "content": read_file(
146 pathlib.Path(framework["path"], static_file),
147 stringify=True,
151 else:
152 frameworks_info[yaml_content["name"]]["static"].append(
154 "file": static_file,
155 "content": pathlib.Path(framework["path"], static_file),
159 return frameworks_info
161 def _create_temp_dir(self):
163 Create a temp directory as preparation of saving the documentation tree.
164 :return: str the location of perfdocs_tmpdir
166 # Build the directory that will contain the final result (a tmp dir
167 # that will be moved to the final location afterwards)
168 try:
169 tmpdir = pathlib.Path(tempfile.mkdtemp())
170 perfdocs_tmpdir = pathlib.Path(tmpdir, "generated")
171 perfdocs_tmpdir.mkdir(parents=True, exist_ok=True)
172 perfdocs_tmpdir.chmod(0o766)
173 except OSError as e:
174 logger.critical("Error creating temp file: {}".format(e))
176 if perfdocs_tmpdir.is_dir():
177 return perfdocs_tmpdir
178 return False
180 def _create_perfdocs(self):
182 Creates the perfdocs documentation.
183 :return: str path of the temp dir it is saved in
185 # All directories that are kept in the perfdocs tree are valid,
186 # so use it to build up the documentation.
187 framework_docs = self.build_perfdocs_from_tree()
188 perfdocs_tmpdir = self._create_temp_dir()
190 # Save the documentation files
191 frameworks = []
192 for framework_name in sorted(framework_docs.keys()):
193 frameworks.append(framework_name)
194 save_file(
195 framework_docs[framework_name]["dynamic"],
196 pathlib.Path(perfdocs_tmpdir, framework_name),
199 if framework_docs[framework_name]["metrics"]:
200 save_file(
201 framework_docs[framework_name]["metrics"],
202 pathlib.Path(perfdocs_tmpdir, f"{framework_name}-metrics"),
205 for static_name in framework_docs[framework_name]["static"]:
206 if static_name["file"].endswith(".rst"):
207 # XXX Replace this with a shutil.copy call (like below)
208 save_file(
209 static_name["content"],
210 pathlib.Path(
211 perfdocs_tmpdir, static_name["file"].split(".")[0]
214 else:
215 shutil.copy(
216 static_name["content"],
217 pathlib.Path(perfdocs_tmpdir, static_name["file"]),
220 # Get the main page and add the framework links to it
221 mainpage = read_file(
222 pathlib.Path(self.templates_path, "index.rst"), stringify=True
225 fmt_frameworks = "\n".join([" * :doc:`%s`" % name for name in frameworks])
226 fmt_toctree = "\n".join([" %s" % name for name in frameworks])
228 fmt_mainpage = re.sub(r"{toctree_documentation}", fmt_toctree, mainpage)
229 fmt_mainpage = re.sub(r"{test_documentation}", fmt_frameworks, fmt_mainpage)
231 save_file(fmt_mainpage, pathlib.Path(perfdocs_tmpdir, "index"))
233 return perfdocs_tmpdir
235 def _save_perfdocs(self, perfdocs_tmpdir):
237 Copies the perfdocs tree after it was saved into the perfdocs_tmpdir
238 :param perfdocs_tmpdir: str location of the temp dir where the
239 perfdocs was saved
241 # Remove the old docs and copy the new version there without
242 # checking if they need to be regenerated.
243 logger.log("Regenerating perfdocs...")
245 if self.perfdocs_path.exists():
246 shutil.rmtree(str(self.perfdocs_path))
248 try:
249 saved = shutil.copytree(str(perfdocs_tmpdir), str(self.perfdocs_path))
250 if saved:
251 logger.log(
252 "Documentation saved to {}/".format(
253 re.sub(".*testing", "testing", str(self.perfdocs_path))
256 except Exception as e:
257 logger.critical(
258 "There was an error while saving the documentation: {}".format(e)
261 def generate_perfdocs(self):
263 Generate the performance documentation.
265 If `self._generate` is True, then the documentation will be regenerated
266 without any checks. Otherwise, if it is False, the new documentation will be
267 prepare and compare with the existing documentation to determine if
268 it should be regenerated.
270 :return bool: True/False - For True, if `self._generate` is True, then the
271 docs were regenerated. If `self._generate` is False, then True will mean
272 that the docs should be regenerated, and False means that they do not
273 need to be regenerated.
276 def get_possibly_changed_files():
278 Returns files that might have been modified
279 (used to output a linter warning for regeneration)
280 :return: list - files that might have been modified
282 # Returns files that might have been modified
283 # (used to output a linter warning for regeneration)
284 files = []
285 for entry in self._perfdocs_tree:
286 files.extend(
288 pathlib.Path(entry["path"], entry["yml"]),
289 pathlib.Path(entry["path"], entry["rst"]),
292 return files
294 # Throw a warning if there's no need for generating
295 if not self.perfdocs_path.exists() and not self._generate:
296 # If they don't exist and we are not generating, then throw
297 # a linting error and exit.
298 logger.warning(
299 "PerfDocs need to be regenerated.", files=get_possibly_changed_files()
301 return True
303 perfdocs_tmpdir = self._create_perfdocs()
304 if self._generate:
305 self._save_perfdocs(perfdocs_tmpdir)
306 elif not are_dirs_equal(perfdocs_tmpdir, self.perfdocs_path):
307 # If we are not generating, then at least check if they
308 # should be regenerated by comparing the directories.
309 logger.warning(
310 "PerfDocs are outdated, run ./mach lint -l perfdocs --fix .` "
311 + "to update them. You can also apply the "
312 + f"{'perfdocs.diff' if ON_TRY else 'diff.txt'} patch file "
313 + f"{'produced from this reviewbot test ' if ON_TRY else ''}"
314 + "to fix the issue.",
315 files=get_changed_files(self._workspace),
316 restricted=False,