summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozsystemmonitor
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozsystemmonitor')
-rw-r--r--testing/mozbase/mozsystemmonitor/README.rst13
-rw-r--r--testing/mozbase/mozsystemmonitor/mozsystemmonitor/__init__.py0
-rw-r--r--testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py744
-rw-r--r--testing/mozbase/mozsystemmonitor/setup.cfg2
-rw-r--r--testing/mozbase/mozsystemmonitor/setup.py36
-rw-r--r--testing/mozbase/mozsystemmonitor/tests/manifest.ini3
-rw-r--r--testing/mozbase/mozsystemmonitor/tests/test_resource_monitor.py188
7 files changed, 986 insertions, 0 deletions
diff --git a/testing/mozbase/mozsystemmonitor/README.rst b/testing/mozbase/mozsystemmonitor/README.rst
new file mode 100644
index 0000000000..65756d44d3
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/README.rst
@@ -0,0 +1,13 @@
+================
+mozsystemmonitor
+================
+
+mozsystemmonitor contains modules for monitoring a running system.
+
+SystemResourceMonitor
+=====================
+
+mozsystemmonitor.resourcemonitor.SystemResourceMonitor is class used to
+measure system resource usage. It is useful to get a handle on what an
+overall system is doing.
+
diff --git a/testing/mozbase/mozsystemmonitor/mozsystemmonitor/__init__.py b/testing/mozbase/mozsystemmonitor/mozsystemmonitor/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/mozsystemmonitor/__init__.py
diff --git a/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py b/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py
new file mode 100644
index 0000000000..eb7b8ffb5d
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/mozsystemmonitor/resourcemonitor.py
@@ -0,0 +1,744 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, division
+
+from contextlib import contextmanager
+import multiprocessing
+import sys
+import time
+import warnings
+
+from collections import (
+ OrderedDict,
+ namedtuple,
+)
+
+
+class PsutilStub(object):
+ def __init__(self):
+ self.sswap = namedtuple(
+ "sswap", ["total", "used", "free", "percent", "sin", "sout"]
+ )
+ self.sdiskio = namedtuple(
+ "sdiskio",
+ [
+ "read_count",
+ "write_count",
+ "read_bytes",
+ "write_bytes",
+ "read_time",
+ "write_time",
+ ],
+ )
+ self.pcputimes = namedtuple("pcputimes", ["user", "system"])
+ self.svmem = namedtuple(
+ "svmem",
+ [
+ "total",
+ "available",
+ "percent",
+ "used",
+ "free",
+ "active",
+ "inactive",
+ "buffers",
+ "cached",
+ ],
+ )
+
+ def cpu_percent(self, a, b):
+ return [0]
+
+ def cpu_times(self, percpu):
+ if percpu:
+ return [self.pcputimes(0, 0)]
+ else:
+ return self.pcputimes(0, 0)
+
+ def disk_io_counters(self):
+ return self.sdiskio(0, 0, 0, 0, 0, 0)
+
+ def swap_memory(self):
+ return self.sswap(0, 0, 0, 0, 0, 0)
+
+ def virtual_memory(self):
+ return self.svmem(0, 0, 0, 0, 0, 0, 0, 0, 0)
+
+
+# psutil will raise NotImplementedError if the platform is not supported.
+try:
+ import psutil
+
+ have_psutil = True
+except Exception:
+ try:
+ # The PsutilStub should get us time intervals, at least
+ psutil = PsutilStub()
+ except Exception:
+ psutil = None
+
+ have_psutil = False
+
+
+def get_disk_io_counters():
+ try:
+ io_counters = psutil.disk_io_counters()
+
+ if io_counters is None:
+ return PsutilStub().disk_io_counters()
+ except RuntimeError:
+ io_counters = PsutilStub().disk_io_counters()
+
+ return io_counters
+
+
+def _poll(pipe, poll_interval=0.1):
+ """Wrap multiprocessing.Pipe.poll to hide POLLERR and POLLIN
+ exceptions.
+
+ multiprocessing.Pipe is not actually a pipe on at least Linux.
+ That has an effect on the expected outcome of reading from it when
+ the other end of the pipe dies, leading to possibly hanging on revc()
+ below.
+ """
+ try:
+ return pipe.poll(poll_interval)
+ except Exception:
+ # Poll might throw an exception even though there's still
+ # data to read. That happens when the underlying system call
+ # returns both POLLERR and POLLIN, but python doesn't tell us
+ # about it. So assume there is something to read, and we'll
+ # get an exception when trying to read the data.
+ return True
+
+
+def _collect(pipe, poll_interval):
+ """Collects system metrics.
+
+ This is the main function for the background process. It collects
+ data then forwards it on a pipe until told to stop.
+ """
+
+ data = []
+
+ # Establish initial values.
+
+ # We should ideally use a monotonic clock. However, Python 2.7 doesn't
+ # make a monotonic clock available on all platforms. Python 3.3 does!
+ last_time = time.time()
+ io_last = get_disk_io_counters()
+ cpu_last = psutil.cpu_times(True)
+ swap_last = psutil.swap_memory()
+ psutil.cpu_percent(None, True)
+
+ sin_index = swap_last._fields.index("sin")
+ sout_index = swap_last._fields.index("sout")
+
+ sleep_interval = poll_interval
+
+ while not _poll(pipe, poll_interval=sleep_interval):
+ io = get_disk_io_counters()
+ cpu_times = psutil.cpu_times(True)
+ cpu_percent = psutil.cpu_percent(None, True)
+ virt_mem = psutil.virtual_memory()
+ swap_mem = psutil.swap_memory()
+ measured_end_time = time.time()
+
+ # TODO Does this wrap? At 32 bits? At 64 bits?
+ # TODO Consider patching "delta" API to upstream.
+ io_diff = [v - io_last[i] for i, v in enumerate(io)]
+ io_last = io
+
+ cpu_diff = []
+ for core, values in enumerate(cpu_times):
+ cpu_diff.append([v - cpu_last[core][i] for i, v in enumerate(values)])
+
+ cpu_last = cpu_times
+
+ swap_entry = list(swap_mem)
+ swap_entry[sin_index] = swap_mem.sin - swap_last.sin
+ swap_entry[sout_index] = swap_mem.sout - swap_last.sout
+ swap_last = swap_mem
+
+ data.append(
+ (
+ last_time,
+ measured_end_time,
+ io_diff,
+ cpu_diff,
+ cpu_percent,
+ list(virt_mem),
+ swap_entry,
+ )
+ )
+
+ collection_overhead = time.time() - last_time - poll_interval
+ last_time = measured_end_time
+ sleep_interval = max(0, poll_interval - collection_overhead)
+
+ for entry in data:
+ pipe.send(entry)
+
+ pipe.send(("done", None, None, None, None, None, None))
+ pipe.close()
+ sys.exit(0)
+
+
+SystemResourceUsage = namedtuple(
+ "SystemResourceUsage",
+ ["start", "end", "cpu_times", "cpu_percent", "io", "virt", "swap"],
+)
+
+
+class SystemResourceMonitor(object):
+ """Measures system resources.
+
+ Each instance measures system resources from the time it is started
+ until it is finished. It does this on a separate process so it doesn't
+ impact execution of the main Python process.
+
+ Each instance is a one-shot instance. It cannot be used to record multiple
+ durations.
+
+ Aside from basic data gathering, the class supports basic analysis
+ capabilities. You can query for data between ranges. You can also tell it
+ when certain events occur and later grab data relevant to those events or
+ plot those events on a timeline.
+
+ The resource monitor works by periodically polling the state of the
+ system. By default, it polls every second. This can be adjusted depending
+ on the required granularity of the data and considerations for probe
+ overhead. It tries to probe at the interval specified. However, variations
+ should be expected. Fast and well-behaving systems should experience
+ variations in the 1ms range. Larger variations may exist if the system is
+ under heavy load or depending on how accurate socket polling is on your
+ system.
+
+ In its current implementation, data is not available until collection has
+ stopped. This may change in future iterations.
+
+ Usage
+ =====
+
+ monitor = SystemResourceMonitor()
+ monitor.start()
+
+ # Record that a single event in time just occurred.
+ foo.do_stuff()
+ monitor.record_event('foo_did_stuff')
+
+ # Record that we're about to perform a possibly long-running event.
+ with monitor.phase('long_job'):
+ foo.do_long_running_job()
+
+ # Stop recording. Currently we need to stop before data is available.
+ monitor.stop()
+
+ # Obtain the raw data for the entire probed range.
+ print('CPU Usage:')
+ for core in monitor.aggregate_cpu():
+ print(core)
+
+ # We can also request data corresponding to a specific phase.
+ for data in monitor.phase_usage('long_job'):
+ print(data.cpu_percent)
+ """
+
+ # The interprocess communication is complicated enough to warrant
+ # explanation. To work around the Python GIL, we launch a separate
+ # background process whose only job is to collect metrics. If we performed
+ # collection in the main process, the polling interval would be
+ # inconsistent if a long-running function were on the stack. Since the
+ # child process is independent of the instantiating process, data
+ # collection should be evenly spaced.
+ #
+ # As the child process collects data, it buffers it locally. When
+ # collection stops, it flushes all that data to a pipe to be read by
+ # the parent process.
+
+ def __init__(self, poll_interval=1.0):
+ """Instantiate a system resource monitor instance.
+
+ The instance is configured with a poll interval. This is the interval
+ between samples, in float seconds.
+ """
+ self.start_time = None
+ self.end_time = None
+
+ self.events = []
+ self.phases = OrderedDict()
+
+ self._active_phases = {}
+
+ self._running = False
+ self._stopped = False
+ self._process = None
+
+ if psutil is None:
+ return
+
+ # This try..except should not be needed! However, some tools (like
+ # |mach build|) attempt to load psutil before properly creating a
+ # virtualenv by building psutil. As a result, python/psutil may be in
+ # sys.path and its .py files may pick up the psutil C extension from
+ # the system install. If the versions don't match, we typically see
+ # failures invoking one of these functions.
+ try:
+ cpu_percent = psutil.cpu_percent(0.0, True)
+ cpu_times = psutil.cpu_times(False)
+ io = get_disk_io_counters()
+ virt = psutil.virtual_memory()
+ swap = psutil.swap_memory()
+ except Exception as e:
+ warnings.warn("psutil failed to run: %s" % e)
+ return
+
+ self._cpu_cores = len(cpu_percent)
+ self._cpu_times_type = type(cpu_times)
+ self._cpu_times_len = len(cpu_times)
+ self._io_type = type(io)
+ self._io_len = len(io)
+ self._virt_type = type(virt)
+ self._virt_len = len(virt)
+ self._swap_type = type(swap)
+ self._swap_len = len(swap)
+
+ self._pipe, child_pipe = multiprocessing.Pipe(True)
+
+ self._process = multiprocessing.Process(
+ target=_collect, args=(child_pipe, poll_interval)
+ )
+
+ def __del__(self):
+ if self._running:
+ self._pipe.send(("terminate",))
+ self._process.join()
+
+ # Methods to control monitoring.
+
+ def start(self):
+ """Start measuring system-wide CPU resource utilization.
+
+ You should only call this once per instance.
+ """
+ if not self._process:
+ return
+
+ self._running = True
+ self._process.start()
+
+ def stop(self):
+ """Stop measuring system-wide CPU resource utilization.
+
+ You should call this if and only if you have called start(). You should
+ always pair a stop() with a start().
+
+ Currently, data is not available until you call stop().
+ """
+ if not self._process:
+ self._stopped = True
+ return
+
+ assert self._running
+ assert not self._stopped
+
+ try:
+ self._pipe.send(("terminate",))
+ except Exception:
+ pass
+ self._running = False
+ self._stopped = True
+
+ self.measurements = []
+
+ # The child process will send each data sample over the pipe
+ # as a separate data structure. When it has finished sending
+ # samples, it sends a special "done" message to indicate it
+ # is finished.
+
+ while _poll(self._pipe, poll_interval=0.1):
+ try:
+ (
+ start_time,
+ end_time,
+ io_diff,
+ cpu_diff,
+ cpu_percent,
+ virt_mem,
+ swap_mem,
+ ) = self._pipe.recv()
+ except Exception:
+ # Let's assume we're done here
+ break
+
+ # There should be nothing after the "done" message so
+ # terminate.
+ if start_time == "done":
+ break
+
+ io = self._io_type(*io_diff)
+ virt = self._virt_type(*virt_mem)
+ swap = self._swap_type(*swap_mem)
+ cpu_times = [self._cpu_times_type(*v) for v in cpu_diff]
+
+ self.measurements.append(
+ SystemResourceUsage(
+ start_time, end_time, cpu_times, cpu_percent, io, virt, swap
+ )
+ )
+
+ # We establish a timeout so we don't hang forever if the child
+ # process has crashed.
+ self._process.join(10)
+ if self._process.is_alive():
+ self._process.terminate()
+ self._process.join(10)
+
+ if len(self.measurements):
+ self.start_time = self.measurements[0].start
+ self.end_time = self.measurements[-1].end
+
+ # Methods to record events alongside the monitored data.
+
+ def record_event(self, name):
+ """Record an event as occuring now.
+
+ Events are actions that occur at a specific point in time. If you are
+ looking for an action that has a duration, see the phase API below.
+ """
+ self.events.append((time.time(), name))
+
+ @contextmanager
+ def phase(self, name):
+ """Context manager for recording an active phase."""
+ self.begin_phase(name)
+ yield
+ self.finish_phase(name)
+
+ def begin_phase(self, name):
+ """Record the start of a phase.
+
+ Phases are actions that have a duration. Multiple phases can be active
+ simultaneously. Phases can be closed in any order.
+
+ Keep in mind that if phases occur in parallel, it will become difficult
+ to isolate resource utilization specific to individual phases.
+ """
+ assert name not in self._active_phases
+
+ self._active_phases[name] = time.time()
+
+ def finish_phase(self, name):
+ """Record the end of a phase."""
+
+ assert name in self._active_phases
+
+ phase = (self._active_phases[name], time.time())
+ self.phases[name] = phase
+ del self._active_phases[name]
+
+ return phase[1] - phase[0]
+
+ # Methods to query data.
+
+ def range_usage(self, start=None, end=None):
+ """Obtain the usage data falling within the given time range.
+
+ This is a generator of SystemResourceUsage.
+
+ If no time range bounds are given, all data is returned.
+ """
+ if not self._stopped or self.start_time is None:
+ return
+
+ if start is None:
+ start = self.start_time
+
+ if end is None:
+ end = self.end_time
+
+ for entry in self.measurements:
+ if entry.start < start:
+ continue
+
+ if entry.end > end:
+ break
+
+ yield entry
+
+ def phase_usage(self, phase):
+ """Obtain usage data for a specific phase.
+
+ This is a generator of SystemResourceUsage.
+ """
+ time_start, time_end = self.phases[phase]
+
+ return self.range_usage(time_start, time_end)
+
+ def between_events_usage(self, start_event, end_event):
+ """Obtain usage data between two point events.
+
+ This is a generator of SystemResourceUsage.
+ """
+ start_time = None
+ end_time = None
+
+ for t, name in self.events:
+ if name == start_event:
+ start_time = t
+ elif name == end_event:
+ end_time = t
+
+ if start_time is None:
+ raise Exception("Could not find start event: %s" % start_event)
+
+ if end_time is None:
+ raise Exception("Could not find end event: %s" % end_event)
+
+ return self.range_usage(start_time, end_time)
+
+ def aggregate_cpu_percent(self, start=None, end=None, phase=None, per_cpu=True):
+ """Obtain the aggregate CPU percent usage for a range.
+
+ Returns a list of floats representing average CPU usage percentage per
+ core if per_cpu is True (the default). If per_cpu is False, return a
+ single percentage value.
+
+ By default this will return data for the entire instrumented interval.
+ If phase is defined, data for a named phase will be returned. If start
+ and end are defined, these times will be fed into range_usage().
+ """
+ cpu = [[] for i in range(0, self._cpu_cores)]
+
+ if phase:
+ data = self.phase_usage(phase)
+ else:
+ data = self.range_usage(start, end)
+
+ for usage in data:
+ for i, v in enumerate(usage.cpu_percent):
+ cpu[i].append(v)
+
+ samples = len(cpu[0])
+
+ if not samples:
+ return 0
+
+ if per_cpu:
+ # pylint --py3k W1619
+ return [sum(x) / samples for x in cpu]
+
+ cores = [sum(x) for x in cpu]
+
+ # pylint --py3k W1619
+ return sum(cores) / len(cpu) / samples
+
+ def aggregate_cpu_times(self, start=None, end=None, phase=None, per_cpu=True):
+ """Obtain the aggregate CPU times for a range.
+
+ If per_cpu is True (the default), this returns a list of named tuples.
+ Each tuple is as if it were returned by psutil.cpu_times(). If per_cpu
+ is False, this returns a single named tuple of the aforementioned type.
+ """
+ empty = [0 for i in range(0, self._cpu_times_len)]
+ cpu = [list(empty) for i in range(0, self._cpu_cores)]
+
+ if phase:
+ data = self.phase_usage(phase)
+ else:
+ data = self.range_usage(start, end)
+
+ for usage in data:
+ for i, core_values in enumerate(usage.cpu_times):
+ for j, v in enumerate(core_values):
+ cpu[i][j] += v
+
+ if per_cpu:
+ return [self._cpu_times_type(*v) for v in cpu]
+
+ sums = list(empty)
+ for core in cpu:
+ for i, v in enumerate(core):
+ sums[i] += v
+
+ return self._cpu_times_type(*sums)
+
+ def aggregate_io(self, start=None, end=None, phase=None):
+ """Obtain aggregate I/O counters for a range.
+
+ Returns an iostat named tuple from psutil.
+ """
+
+ io = [0 for i in range(self._io_len)]
+
+ if phase:
+ data = self.phase_usage(phase)
+ else:
+ data = self.range_usage(start, end)
+
+ for usage in data:
+ for i, v in enumerate(usage.io):
+ io[i] += v
+
+ return self._io_type(*io)
+
+ def min_memory_available(self, start=None, end=None, phase=None):
+ """Return the minimum observed available memory number from a range.
+
+ Returns long bytes of memory available.
+
+ See psutil for notes on how this is calculated.
+ """
+ if phase:
+ data = self.phase_usage(phase)
+ else:
+ data = self.range_usage(start, end)
+
+ values = []
+
+ for usage in data:
+ values.append(usage.virt.available)
+
+ return min(values)
+
+ def max_memory_percent(self, start=None, end=None, phase=None):
+ """Returns the maximum percentage of system memory used.
+
+ Returns a float percentage. 1.00 would mean all system memory was in
+ use at one point.
+ """
+ if phase:
+ data = self.phase_usage(phase)
+ else:
+ data = self.range_usage(start, end)
+
+ values = []
+
+ for usage in data:
+ values.append(usage.virt.percent)
+
+ return max(values)
+
+ def as_dict(self):
+ """Convert the recorded data to a dict, suitable for serialization.
+
+ The returned dict has the following keys:
+
+ version - Integer version number being rendered. Currently 2.
+ cpu_times_fields - A list of the names of the CPU times fields.
+ io_fields - A list of the names of the I/O fields.
+ virt_fields - A list of the names of the virtual memory fields.
+ swap_fields - A list of the names of the swap memory fields.
+ samples - A list of dicts containing low-level measurements.
+ events - A list of lists representing point events. The inner list
+ has 2 elements, the float wall time of the event and the string
+ event name.
+ phases - A list of dicts describing phases. Each phase looks a lot
+ like an entry from samples (see below). Some phases may not have
+ data recorded against them, so some keys may be None.
+ overall - A dict representing overall resource usage. This resembles
+ a sample entry.
+ system - Contains additional information about the system including
+ number of processors and amount of memory.
+
+ Each entry in the sample list is a dict with the following keys:
+
+ start - Float wall time this measurement began on.
+ end - Float wall time this measurement ended on.
+ io - List of numerics for I/O values.
+ virt - List of numerics for virtual memory values.
+ swap - List of numerics for swap memory values.
+ cpu_percent - List of floats representing CPU percent on each core.
+ cpu_times - List of lists. Main list is each core. Inner lists are
+ lists of floats representing CPU times on that core.
+ cpu_percent_mean - Float of mean CPU percent across all cores.
+ cpu_times_sum - List of floats representing the sum of CPU times
+ across all cores.
+ cpu_times_total - Float representing the sum of all CPU times across
+ all cores. This is useful for calculating the percent in each CPU
+ time.
+ """
+
+ o = dict(
+ version=2,
+ cpu_times_fields=list(self._cpu_times_type._fields),
+ io_fields=list(self._io_type._fields),
+ virt_fields=list(self._virt_type._fields),
+ swap_fields=list(self._swap_type._fields),
+ samples=[],
+ phases=[],
+ system={},
+ )
+
+ def populate_derived(e):
+ if e["cpu_percent_cores"]:
+ # pylint --py3k W1619
+ e["cpu_percent_mean"] = sum(e["cpu_percent_cores"]) / len(
+ e["cpu_percent_cores"]
+ )
+ else:
+ e["cpu_percent_mean"] = None
+
+ if e["cpu_times"]:
+ e["cpu_times_sum"] = [0.0] * self._cpu_times_len
+ for i in range(0, self._cpu_times_len):
+ e["cpu_times_sum"][i] = sum(core[i] for core in e["cpu_times"])
+
+ e["cpu_times_total"] = sum(e["cpu_times_sum"])
+
+ def phase_entry(name, start, end):
+ e = dict(
+ name=name,
+ start=start,
+ end=end,
+ duration=end - start,
+ cpu_percent_cores=self.aggregate_cpu_percent(phase=name),
+ cpu_times=[list(c) for c in self.aggregate_cpu_times(phase=name)],
+ io=list(self.aggregate_io(phase=name)),
+ )
+ populate_derived(e)
+ return e
+
+ for m in self.measurements:
+ e = dict(
+ start=m.start,
+ end=m.end,
+ io=list(m.io),
+ virt=list(m.virt),
+ swap=list(m.swap),
+ cpu_percent_cores=list(m.cpu_percent),
+ cpu_times=list(list(cpu) for cpu in m.cpu_times),
+ )
+
+ populate_derived(e)
+ o["samples"].append(e)
+
+ if o["samples"]:
+ o["start"] = o["samples"][0]["start"]
+ o["end"] = o["samples"][-1]["end"]
+ o["duration"] = o["end"] - o["start"]
+ o["overall"] = phase_entry(None, o["start"], o["end"])
+ else:
+ o["start"] = None
+ o["end"] = None
+ o["duration"] = None
+ o["overall"] = None
+
+ o["events"] = [list(ev) for ev in self.events]
+
+ for phase, v in self.phases.items():
+ o["phases"].append(phase_entry(phase, v[0], v[1]))
+
+ if have_psutil:
+ o["system"].update(
+ dict(
+ cpu_logical_count=psutil.cpu_count(logical=True),
+ cpu_physical_count=psutil.cpu_count(logical=False),
+ swap_total=psutil.swap_memory()[0],
+ vmem_total=psutil.virtual_memory()[0],
+ )
+ )
+
+ return o
diff --git a/testing/mozbase/mozsystemmonitor/setup.cfg b/testing/mozbase/mozsystemmonitor/setup.cfg
new file mode 100644
index 0000000000..3c6e79cf31
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/testing/mozbase/mozsystemmonitor/setup.py b/testing/mozbase/mozsystemmonitor/setup.py
new file mode 100644
index 0000000000..85835e232c
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/setup.py
@@ -0,0 +1,36 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+
+import os
+
+from setuptools import setup
+
+PACKAGE_VERSION = "1.0.0"
+
+try:
+ pwd = os.path.dirname(os.path.abspath(__file__))
+ description = open(os.path.join(pwd, "README.rst")).read()
+except Exception:
+ description = ""
+
+setup(
+ name="mozsystemmonitor",
+ description="Monitor system resource usage.",
+ long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html",
+ classifiers=[
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.5",
+ ],
+ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ license="MPL 2.0",
+ keywords="mozilla",
+ author="Mozilla Automation and Tools Team",
+ author_email="tools@lists.mozilla.org",
+ url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase",
+ packages=["mozsystemmonitor"],
+ version=PACKAGE_VERSION,
+ install_requires=["psutil >= 3.1.1"],
+)
diff --git a/testing/mozbase/mozsystemmonitor/tests/manifest.ini b/testing/mozbase/mozsystemmonitor/tests/manifest.ini
new file mode 100644
index 0000000000..4e265d7570
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/tests/manifest.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+subsuite = mozbase
+[test_resource_monitor.py]
diff --git a/testing/mozbase/mozsystemmonitor/tests/test_resource_monitor.py b/testing/mozbase/mozsystemmonitor/tests/test_resource_monitor.py
new file mode 100644
index 0000000000..59951a7eb6
--- /dev/null
+++ b/testing/mozbase/mozsystemmonitor/tests/test_resource_monitor.py
@@ -0,0 +1,188 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+
+import multiprocessing
+import time
+import unittest
+
+import mozunit
+from six import integer_types
+
+try:
+ import psutil
+except ImportError:
+ psutil = None
+
+from mozsystemmonitor.resourcemonitor import (
+ SystemResourceMonitor,
+ SystemResourceUsage,
+)
+
+
+@unittest.skipIf(psutil is None, "Resource monitor requires psutil.")
+class TestResourceMonitor(unittest.TestCase):
+ def test_basic(self):
+ monitor = SystemResourceMonitor(poll_interval=0.5)
+
+ monitor.start()
+ time.sleep(3)
+
+ monitor.stop()
+
+ data = list(monitor.range_usage())
+ self.assertGreater(len(data), 3)
+
+ self.assertIsInstance(data[0], SystemResourceUsage)
+
+ def test_empty(self):
+ monitor = SystemResourceMonitor(poll_interval=2.0)
+ monitor.start()
+ monitor.stop()
+
+ data = list(monitor.range_usage())
+ self.assertEqual(len(data), 0)
+
+ def test_phases(self):
+ monitor = SystemResourceMonitor(poll_interval=0.25)
+
+ monitor.start()
+ time.sleep(1)
+
+ with monitor.phase("phase1"):
+ time.sleep(1)
+
+ with monitor.phase("phase2"):
+ time.sleep(1)
+
+ monitor.stop()
+
+ self.assertEqual(len(monitor.phases), 2)
+ self.assertEqual(["phase2", "phase1"], list(monitor.phases.keys()))
+
+ all = list(monitor.range_usage())
+ data1 = list(monitor.phase_usage("phase1"))
+ data2 = list(monitor.phase_usage("phase2"))
+
+ self.assertGreater(len(all), len(data1))
+ self.assertGreater(len(data1), len(data2))
+
+ # This could fail if time.time() takes more than 0.1s. It really
+ # shouldn't.
+ self.assertAlmostEqual(data1[-1].end, data2[-1].end, delta=0.25)
+
+ def test_no_data(self):
+ monitor = SystemResourceMonitor()
+
+ data = list(monitor.range_usage())
+ self.assertEqual(len(data), 0)
+
+ def test_events(self):
+ monitor = SystemResourceMonitor(poll_interval=0.25)
+
+ monitor.start()
+ time.sleep(0.5)
+
+ t0 = time.time()
+ monitor.record_event("t0")
+ time.sleep(2)
+
+ monitor.record_event("t1")
+ time.sleep(0.5)
+ monitor.stop()
+
+ events = monitor.events
+ self.assertEqual(len(events), 2)
+
+ event = events[0]
+
+ self.assertEqual(event[1], "t0")
+ self.assertAlmostEqual(event[0], t0, delta=0.25)
+
+ data = list(monitor.between_events_usage("t0", "t1"))
+ self.assertGreater(len(data), 0)
+
+ def test_aggregate_cpu(self):
+ monitor = SystemResourceMonitor(poll_interval=0.25)
+
+ monitor.start()
+ time.sleep(1)
+ monitor.stop()
+
+ values = monitor.aggregate_cpu_percent()
+ self.assertIsInstance(values, list)
+ self.assertEqual(len(values), multiprocessing.cpu_count())
+ for v in values:
+ self.assertIsInstance(v, float)
+
+ value = monitor.aggregate_cpu_percent(per_cpu=False)
+ self.assertIsInstance(value, float)
+
+ values = monitor.aggregate_cpu_times()
+ self.assertIsInstance(values, list)
+ self.assertGreater(len(values), 0)
+ self.assertTrue(hasattr(values[0], "user"))
+
+ t = type(values[0])
+
+ value = monitor.aggregate_cpu_times(per_cpu=False)
+ self.assertIsInstance(value, t)
+
+ def test_aggregate_io(self):
+ monitor = SystemResourceMonitor(poll_interval=0.25)
+
+ # There's really no easy way to ensure I/O occurs. For all we know
+ # reads and writes will all be serviced by the page cache.
+ monitor.start()
+ time.sleep(1.0)
+ monitor.stop()
+
+ values = monitor.aggregate_io()
+ self.assertTrue(hasattr(values, "read_count"))
+
+ def test_memory(self):
+ monitor = SystemResourceMonitor(poll_interval=0.25)
+
+ monitor.start()
+ time.sleep(1.0)
+ monitor.stop()
+
+ v = monitor.min_memory_available()
+ self.assertIsInstance(v, integer_types)
+
+ v = monitor.max_memory_percent()
+ self.assertIsInstance(v, float)
+
+ def test_as_dict(self):
+ monitor = SystemResourceMonitor(poll_interval=0.25)
+
+ monitor.start()
+ time.sleep(0.1)
+ monitor.begin_phase("phase1")
+ monitor.record_event("foo")
+ time.sleep(0.1)
+ monitor.begin_phase("phase2")
+ monitor.record_event("bar")
+ time.sleep(0.2)
+ monitor.finish_phase("phase1")
+ time.sleep(0.2)
+ monitor.finish_phase("phase2")
+ time.sleep(0.4)
+ monitor.stop()
+
+ d = monitor.as_dict()
+
+ self.assertEqual(d["version"], 2)
+ self.assertEqual(len(d["events"]), 2)
+ self.assertEqual(len(d["phases"]), 2)
+ self.assertIn("system", d)
+ self.assertIsInstance(d["system"], dict)
+ self.assertIsInstance(d["overall"], dict)
+ self.assertIn("duration", d["overall"])
+ self.assertIn("cpu_times", d["overall"])
+
+
+if __name__ == "__main__":
+ mozunit.main()