1 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
2 # vim: set filetype=python:
3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
17 import mozlog
.structured
18 from marionette_driver
import Wait
19 from marionette_driver
.errors
import JavascriptException
, ScriptTimeoutException
20 from marionette_driver
.keys
import Keys
21 from marionette_harness
import MarionetteTestCase
23 AWSY_PATH
= os
.path
.dirname(os
.path
.dirname(os
.path
.realpath(__file__
)))
24 if AWSY_PATH
not in sys
.path
:
25 sys
.path
.append(AWSY_PATH
)
36 class AwsyTestCase(MarionetteTestCase
):
38 Base test case for AWSY tests.
42 raise NotImplementedError()
44 def perf_suites(self
):
45 raise NotImplementedError()
47 def perf_checkpoints(self
):
48 raise NotImplementedError()
50 def perf_extra_opts(self
):
54 return self
._iterations
56 def pages_to_load(self
):
57 return self
._pages
_to
_load
if self
._pages
_to
_load
else len(self
.urls())
61 Pauses for the settle time.
63 time
.sleep(self
._settleWaitTime
)
66 MarionetteTestCase
.setUp(self
)
68 self
.logger
= mozlog
.structured
.structuredlog
.get_default_logger()
69 self
.marionette
.set_context("chrome")
70 self
._resultsDir
= self
.testvars
["resultsDir"]
72 self
._binary
= self
.testvars
["bin"]
73 self
._run
_local
= self
.testvars
.get("run_local", False)
75 # Cleanup our files from previous runs.
77 "memory-report-*.json.gz",
78 "perfherder-data.json",
81 for f
in glob
.glob(os
.path
.join(self
._resultsDir
, patt
)):
85 self
._pages
_to
_load
= self
.testvars
.get("entities", 0)
86 self
._iterations
= self
.testvars
.get("iterations", ITERATIONS
)
87 self
._perTabPause
= self
.testvars
.get("perTabPause", PER_TAB_PAUSE
)
88 self
._settleWaitTime
= self
.testvars
.get("settleWaitTime", SETTLE_WAIT_TIME
)
89 self
._maxTabs
= self
.testvars
.get("maxTabs", MAX_TABS
)
90 self
._dmd
= self
.testvars
.get("dmd", False)
93 "areweslimyet run by %d pages, %d iterations,"
94 " %d perTabPause, %d settleWaitTime"
105 MarionetteTestCase
.tearDown(self
)
108 self
.logger
.info("processing data in %s!" % self
._resultsDir
)
109 perf_blob
= process_perf_data
.create_perf_data(
112 self
.perf_checkpoints(),
113 self
.perf_extra_opts(),
115 self
.logger
.info("PERFHERDER_DATA: %s" % json
.dumps(perf_blob
))
117 perf_file
= os
.path
.join(self
._resultsDir
, "perfherder-data.json")
118 with
open(perf_file
, "w") as fp
:
119 json
.dump(perf_blob
, fp
, indent
=2)
120 self
.logger
.info("Perfherder data written to %s" % perf_file
)
124 # Make sure we cleanup and upload any existing files even if there
125 # were errors processing the perf data.
129 # copy it to moz upload dir if set
130 if "MOZ_UPLOAD_DIR" in os
.environ
:
131 for file in os
.listdir(self
._resultsDir
):
132 file = os
.path
.join(self
._resultsDir
, file)
133 if os
.path
.isfile(file):
134 shutil
.copy2(file, os
.environ
["MOZ_UPLOAD_DIR"])
136 def cleanup_dmd(self
):
138 Handles moving DMD reports from the temp dir to our resultsDir.
140 from dmd
import fixStackTraces
142 # Move DMD files from temp dir to resultsDir.
143 tmpdir
= tempfile
.gettempdir()
144 tmp_files
= os
.listdir(tmpdir
)
145 for f
in fnmatch
.filter(tmp_files
, "dmd-*.json.gz"):
146 f
= os
.path
.join(tmpdir
, f
)
147 # We don't fix stacks on Windows, even though we could, due to the
148 # tale of woe in bug 1626272.
149 if not sys
.platform
.startswith("win"):
150 self
.logger
.info("Fixing stacks for %s, this may take a while" % f
)
152 fixStackTraces(f
, isZipped
, gzip
.open)
153 shutil
.move(f
, self
._resultsDir
)
155 # Also attempt to cleanup the unified memory reports.
156 for f
in fnmatch
.filter(tmp_files
, "unified-memory-report-*.json.gz"):
160 self
.logger
.info("Unable to remove %s" % f
)
162 def reset_state(self
):
163 self
._pages
_loaded
= 0
165 # Close all tabs except one
166 for x
in self
.marionette
.window_handles
[1:]:
167 self
.logger
.info("closing window: %s" % x
)
168 self
.marionette
.switch_to_window(x
)
169 self
.marionette
.close()
171 self
._tabs
= self
.marionette
.window_handles
172 self
.marionette
.switch_to_window(self
._tabs
[0])
174 def do_memory_report(self
, checkpointName
, iteration
, minimize
=False):
175 """Creates a memory report for all processes and and returns the
178 This will block until all reports are retrieved or a timeout occurs.
179 Returns the checkpoint or None on error.
181 :param checkpointName: The name of the checkpoint.
183 :param minimize: If true, minimize memory before getting the report.
185 self
.logger
.info("starting checkpoint %s..." % checkpointName
)
187 checkpoint_file
= "memory-report-%s-%d.json.gz" % (checkpointName
, iteration
)
188 checkpoint_path
= os
.path
.join(self
._resultsDir
, checkpoint_file
)
189 # On Windows, replace / with the Windows directory
190 # separator \ and escape it to prevent it from being
191 # interpreted as an escape character.
192 if sys
.platform
.startswith("win"):
193 checkpoint_path
= checkpoint_path
.replace("\\", "\\\\").replace("/", "\\\\")
195 checkpoint_script
= r
"""
196 let [resolve] = arguments;
198 Cc["@mozilla.org/memory-info-dumper;1"].getService(
199 Ci.nsIMemoryInfoDumper);
200 dumper.dumpMemoryReportsToNamedFile(
202 () => resolve("memory report done!"),
204 /* anonymize */ false,
205 /* minimize memory usage */ %s);
208 "true" if minimize
else "false",
213 finished
= self
.marionette
.execute_async_script(
214 checkpoint_script
, script_timeout
=60000
217 checkpoint
= checkpoint_path
218 except JavascriptException
as e
:
219 self
.logger
.error("Checkpoint JavaScript error: %s" % e
)
220 except ScriptTimeoutException
:
221 self
.logger
.error("Memory report timed out")
223 self
.logger
.error("Unexpected error: %s" % sys
.exc_info()[0])
225 self
.logger
.info("checkpoint created, stored in %s" % checkpoint_path
)
227 # Now trigger a DMD report if requested.
229 self
.do_dmd(checkpointName
, iteration
)
233 def do_dmd(self
, checkpointName
, iteration
):
235 Triggers DMD reports that are used to help identify sources of
238 NB: This will dump DMD reports to the temp dir. Unfortunately it also
239 dumps memory reports, but that's all we have to work with right now.
241 self
.logger
.info("Starting %s DMD reports..." % checkpointName
)
243 ident
= "%s-%d" % (checkpointName
, iteration
)
245 # TODO(ER): This actually takes a minimize argument. We could use that
246 # rather than have a separate `do_gc` function. Also it generates a
247 # memory report so we could combine this with `do_checkpoint`. The main
248 # issue would be moving everything out of the temp dir.
250 # Generated files have the form:
251 # dmd-<checkpoint>-<iteration>-pid.json.gz, ie:
252 # dmd-TabsOpenForceGC-0-10885.json.gz
254 # and for the memory report:
255 # unified-memory-report-<checkpoint>-<iteration>.json.gz
259 Cc["@mozilla.org/memory-info-dumper;1"].getService(
260 Ci.nsIMemoryInfoDumper);
261 dumper.dumpMemoryInfoToTempDir(
263 /* anonymize = */ false,
264 /* minimize = */ false);
270 # This is async and there's no callback so we use the existence
271 # of an incomplete memory report to check if it hasn't finished yet.
272 self
.marionette
.execute_script(dmd_script
, script_timeout
=60000)
273 tmpdir
= tempfile
.gettempdir()
274 prefix
= "incomplete-unified-memory-report-%s-%d-*" % (
280 while fnmatch
.filter(os
.listdir(tmpdir
), prefix
) and elapsed
< max_wait
:
281 self
.logger
.info("Waiting for memory report to finish")
285 incomplete
= fnmatch
.filter(os
.listdir(tmpdir
), prefix
)
287 # The memory reports never finished.
288 self
.logger
.error("Incomplete memory reports leftover.")
290 os
.remove(os
.path
.join(tmpdir
, f
))
292 except JavascriptException
as e
:
293 self
.logger
.error("DMD JavaScript error: %s" % e
)
294 except ScriptTimeoutException
:
295 self
.logger
.error("DMD timed out")
297 self
.logger
.error("Unexpected error: %s" % sys
.exc_info()[0])
299 self
.logger
.info("DMD started, prefixed with %s" % ident
)
301 def open_and_focus(self
):
302 """Opens the next URL in the list and focuses on the tab it is opened in.
304 A new tab will be opened if |_maxTabs| has not been exceeded, otherwise
305 the URL will be loaded in the next tab.
307 page_to_load
= self
.urls()[self
._pages
_loaded
% len(self
.urls())]
308 tabs_loaded
= len(self
._tabs
)
309 open_tab_script
= r
"""
310 gBrowser.addTab("about:blank", {
312 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
316 if tabs_loaded
< self
._maxTabs
and tabs_loaded
<= self
._pages
_loaded
:
317 full_tab_list
= self
.marionette
.window_handles
319 self
.marionette
.execute_script(open_tab_script
, script_timeout
=60000)
321 Wait(self
.marionette
).until(
322 lambda mn
: len(mn
.window_handles
) == tabs_loaded
+ 1,
323 message
="No new tab has been opened",
326 # NB: The tab list isn't sorted, so we do a set diff to determine
327 # which is the new tab
328 new_tab_list
= self
.marionette
.window_handles
329 new_tabs
= list(set(new_tab_list
) - set(full_tab_list
))
331 self
._tabs
.append(new_tabs
[0])
334 tab_idx
= self
._pages
_loaded
% self
._maxTabs
336 tab
= self
._tabs
[tab_idx
]
338 # Tell marionette which tab we're on
339 # NB: As a work-around for an e10s marionette bug, only select the tab
340 # if we're really switching tabs.
342 self
.logger
.info("switching to tab")
343 self
.marionette
.switch_to_window(tab
)
344 self
.logger
.info("switched to tab")
346 with self
.marionette
.using_context("content"):
347 self
.logger
.info("loading %s" % page_to_load
)
348 self
.marionette
.navigate(page_to_load
)
349 self
.logger
.info("loaded!")
351 # The tab handle can change after actually loading content
352 # First build a set up w/o the current tab
353 old_tabs
= set(self
._tabs
)
355 # Perform a set diff to get the (possibly) new handle
356 new_tabs
= set(self
.marionette
.window_handles
) - old_tabs
357 # Update the tab list at the current index to preserve the tab
360 self
._tabs
[tab_idx
] = list(new_tabs
)[0]
362 # give the page time to settle
363 time
.sleep(self
._perTabPause
)
365 self
._pages
_loaded
+= 1
367 def signal_user_active(self
):
368 """Signal to the browser that the user is active.
370 Normally when being driven by marionette the browser thinks the
371 user is inactive the whole time because user activity is
372 detected by looking at key and mouse events.
374 This would be a problem for this test because user inactivity is
375 used to schedule some GCs (in particular shrinking GCs), so it
376 would make this unrepresentative of real use.
378 Instead we manually cause some inconsequential activity (a press
379 and release of the shift key) to make the browser think the user
380 is active. Then when we sleep to allow things to settle the
381 browser will see the user as becoming inactive and trigger
382 appropriate GCs, as would have happened in real use.
385 action
= self
.marionette
.actions
.sequence("key", "keyboard_id")
386 action
.key_down(Keys
.SHIFT
)
387 action
.key_up(Keys
.SHIFT
)
390 self
.marionette
.actions
.release()
392 def open_pages(self
):
394 Opens all pages with our given configuration.
396 for _
in range(self
.pages_to_load()):
397 self
.open_and_focus()
398 self
.signal_user_active()