Merge pull request #151 from AnthonyARM/master

Add support for arbitrary ADB servers
diff --git a/devlib/__init__.py b/devlib/__init__.py
index 911c50d..2f50632 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -17,6 +17,7 @@
 from devlib.instrument.hwmon import HwmonInstrument
 from devlib.instrument.monsoon import MonsoonInstrument
 from devlib.instrument.netstats import NetstatsInstrument
+from devlib.instrument.gem5power import Gem5PowerInstrument
 
 from devlib.trace.ftrace import FtraceCollector
 
diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py
new file mode 100644
index 0000000..6411b3f
--- /dev/null
+++ b/devlib/instrument/gem5power.py
@@ -0,0 +1,74 @@
+#    Copyright 2017 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import division
+import csv
+import re
+
+from devlib.platform.gem5 import Gem5SimulationPlatform
+from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
+from devlib.exception import TargetError, HostError
+
+
+class Gem5PowerInstrument(Instrument):
+    '''
+    Instrument enabling power monitoring in gem5
+    '''
+
+    mode = CONTINUOUS
+    roi_label = 'power_instrument'
+    
+    def __init__(self, target, power_sites):
+        '''
+        Parameter power_sites is a list of gem5 identifiers for power values. 
+        One example of such a field:
+            system.cluster0.cores0.power_model.static_power
+        '''
+        if not isinstance(target.platform, Gem5SimulationPlatform):
+            raise TargetError('Gem5PowerInstrument requires a gem5 platform')
+        if not target.has('gem5stats'):
+            raise TargetError('Gem5StatsModule is not loaded')
+        super(Gem5PowerInstrument, self).__init__(target)
+
+        # power_sites is assumed to be a list later
+        if isinstance(power_sites, list):
+            self.power_sites = power_sites
+        else:
+            self.power_sites = [power_sites]
+        self.add_channel('sim_seconds', 'time')
+        for field in self.power_sites:
+            self.add_channel(field, 'power')
+        self.target.gem5stats.book_roi(self.roi_label)
+        self.sample_period_ns = 10000000
+        self.target.gem5stats.start_periodic_dump(0, self.sample_period_ns)
+
+    def start(self):
+        self.target.gem5stats.roi_start(self.roi_label)
+
+    def stop(self):
+        self.target.gem5stats.roi_end(self.roi_label)
+       
+    def get_data(self, outfile):
+        active_sites = [c.site for c in self.active_channels]
+        with open(outfile, 'wb') as wfh:
+            writer = csv.writer(wfh)
+            writer.writerow([c.label for c in self.active_channels]) # headers
+            for rec, rois in self.target.gem5stats.match_iter(active_sites, [self.roi_label]):
+                writer.writerow([float(rec[s]) for s in active_sites])
+        return MeasurementsCsv(outfile, self.active_channels)
+    
+    def reset(self, sites=None, kinds=None, channels=None):
+        super(Gem5PowerInstrument, self).reset(sites, kinds, channels)
+        self.target.gem5stats.reset_origin()
+
diff --git a/devlib/module/cpufreq.py b/devlib/module/cpufreq.py
index 46873af..a8028f3 100644
--- a/devlib/module/cpufreq.py
+++ b/devlib/module/cpufreq.py
@@ -412,8 +412,19 @@
         """
         return self.target._execute_util('cpufreq_trace_all_frequencies', as_root=True)
 
+    def get_affected_cpus(self, cpu):
+        """
+        Get the online CPUs that share a frequency domain with the given CPU
+        """
+        if isinstance(cpu, int):
+            cpu = 'cpu{}'.format(cpu)
+
+        sysfile = '/sys/devices/system/cpu/{}/cpufreq/affected_cpus'.format(cpu)
+
+        return [int(c) for c in self.target.read_value(sysfile).split()]
+
     @memoized
-    def get_domain_cpus(self, cpu):
+    def get_related_cpus(self, cpu):
         """
         Get the CPUs that share a frequency domain with the given CPU
         """
@@ -431,6 +442,6 @@
         cpus = set(range(self.target.number_of_cpus))
         while cpus:
             cpu = iter(cpus).next()
-            domain = self.target.cpufreq.get_domain_cpus(cpu)
+            domain = self.target.cpufreq.get_related_cpus(cpu)
             yield domain
             cpus = cpus.difference(domain)
diff --git a/devlib/module/gem5stats.py b/devlib/module/gem5stats.py
new file mode 100644
index 0000000..f919905
--- /dev/null
+++ b/devlib/module/gem5stats.py
@@ -0,0 +1,165 @@
+#    Copyright 2017 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import os.path
+from collections import defaultdict
+
+import devlib
+from devlib.exception import TargetError
+from devlib.module import Module
+from devlib.platform import Platform
+from devlib.platform.gem5 import Gem5SimulationPlatform
+from devlib.utils.gem5 import iter_statistics_dump, GEM5STATS_ROI_NUMBER, GEM5STATS_DUMP_TAIL
+
+
+class Gem5ROI:
+    def __init__(self, number, target):
+        self.target = target
+        self.number = number
+        self.running = False
+        self.field = 'ROI::{}'.format(number)
+
+    def start(self):
+        if self.running:
+            return False
+        self.target.execute('m5 roistart {}'.format(self.number))
+        self.running = True
+        return True
+    
+    def stop(self):
+        if not self.running:
+            return False
+        self.target.execute('m5 roiend {}'.format(self.number))
+        self.running = False
+        return True
+
+class Gem5StatsModule(Module):
+    '''
+    Module controlling Region of Interest (ROIs) markers, satistics dump 
+    frequency and parsing statistics log file when using gem5 platforms.
+
+    ROIs are identified by user-defined labels and need to be booked prior to
+    use. The translation of labels into gem5 ROI numbers will be performed
+    internally in order to avoid conflicts between multiple clients.
+    '''
+    name = 'gem5stats'
+
+    @staticmethod
+    def probe(target):
+        return isinstance(target.platform, Gem5SimulationPlatform)
+
+    def __init__(self, target):
+        super(Gem5StatsModule, self).__init__(target)
+        self._current_origin = 0
+        self._stats_file_path = os.path.join(target.platform.gem5_out_dir,
+                                            'stats.txt')
+        self.rois = {}
+
+    def book_roi(self, label):
+        if label in self.rois:
+            raise KeyError('ROI label {} already used'.format(label))
+        if len(self.rois) >= GEM5STATS_ROI_NUMBER:
+            raise RuntimeError('Too many ROIs reserved')
+        all_rois = set(xrange(GEM5STATS_ROI_NUMBER))
+        used_rois = set([roi.number for roi in self.rois.values()])
+        avail_rois = all_rois - used_rois
+        self.rois[label] = Gem5ROI(list(avail_rois)[0], self.target)
+
+    def free_roi(self, label):
+        if label not in self.rois:
+            raise KeyError('ROI label {} not reserved yet'.format(label))
+        self.rois[label].stop()
+        del self.rois[label]
+
+    def roi_start(self, label):
+        if label not in self.rois:
+            raise KeyError('Incorrect ROI label: {}'.format(label))
+        if not self.rois[label].start():
+            raise TargetError('ROI {} was already running'.format(label))
+    
+    def roi_end(self, label):
+        if label not in self.rois:
+            raise KeyError('Incorrect ROI label: {}'.format(label))
+        if not self.rois[label].stop():
+            raise TargetError('ROI {} was not running'.format(label))
+
+    def start_periodic_dump(self, delay_ns=0, period_ns=10000000):
+        # Default period is 10ms because it's roughly what's needed to have
+        # accurate power estimations
+        if delay_ns < 0 or period_ns < 0:
+            msg = 'Delay ({}) and period ({}) for periodic dumps must be positive'
+            raise ValueError(msg.format(delay_ns, period_ns))
+        self.target.execute('m5 dumpresetstats {} {}'.format(delay_ns, period_ns))
+    
+    def match(self, keys, rois_labels):
+        '''
+        Tries to match the list of keys passed as parameter over the statistics
+        dumps covered by selected ROIs since origin. Returns a dict indexed by 
+        key parameters containing a dict indexed by ROI labels containing an 
+        in-order list of records for the key under consideration during the 
+        active intervals of the ROI.
+
+        Keys must match fields in gem5's statistics log file. Key example:
+            system.cluster0.cores0.power_model.static_power
+        '''
+        records = defaultdict(lambda : defaultdict(list))
+        for record, active_rois in self.match_iter(keys, rois_labels):
+            for key in record:
+                for roi_label in active_rois:
+                    records[key][roi_label].append(record[key])
+        return records
+
+    def match_iter(self, keys, rois_labels):
+        '''
+        Yields for each dump since origin a pair containing:
+        1. a dict storing the values corresponding to each of the specified keys
+        2. the list of currently active ROIs among those passed as parameters.
+
+        Keys must match fields in gem5's statistics log file. Key example:
+            system.cluster0.cores0.power_model.static_power
+        '''
+        for label in rois_labels:
+            if label not in self.rois:
+                raise KeyError('Impossible to match ROI label {}'.format(label))
+            if self.rois[label].running:
+                self.logger.warning('Trying to match records in statistics file'
+                        ' while ROI {} is running'.format(label))
+        
+        def roi_active(roi_label, dump):
+            roi = self.rois[roi_label]
+            return (roi.field in dump) and (int(dump[roi.field]) == 1)
+
+        with open(self._stats_file_path, 'r') as stats_file:
+            stats_file.seek(self._current_origin)
+            for dump in iter_statistics_dump(stats_file):
+                active_rois = [l for l in rois_labels if roi_active(l, dump)]
+                if active_rois:
+                    record = {k: dump[k] for k in keys}
+                    yield (record, active_rois)
+
+
+    def reset_origin(self):
+        '''
+        Place origin right after the last full dump in the file
+        '''
+        last_dump_tail = self._current_origin
+        # Dump & reset stats to start from a fresh state
+        self.target.execute('m5 dumpresetstats')
+        with open(self._stats_file_path, 'r') as stats_file:
+            for line in stats_file:
+                if GEM5STATS_DUMP_TAIL in line:
+                    last_dump_tail = stats_file.tell()
+        self._current_origin = last_dump_tail
+
diff --git a/devlib/module/thermal.py b/devlib/module/thermal.py
index 4fa8e15..fa13fbb 100644
--- a/devlib/module/thermal.py
+++ b/devlib/module/thermal.py
@@ -61,8 +61,8 @@
         value = self.target.read_value(self.target.path.join(self.path, 'mode'))
         return value == 'enabled'
 
-    def set_mode(self, enable):
-        value = 'enabled' if enable else 'disabled'
+    def set_enabled(self, enabled=True):
+        value = 'enabled' if enabled else 'disabled'
         self.target.write_value(self.target.path.join(self.path, 'mode'), value)
 
     def get_temperature(self):
@@ -100,5 +100,5 @@
 
     def disable_all_zones(self):
         """Disables all the thermal zones in the target"""
-        for zone in self.zones:
-            zone.set_mode('disabled')
+        for zone in self.zones.itervalues():
+            zone.set_enabled(False)
diff --git a/devlib/target.py b/devlib/target.py
index f3aaea0..27b1c4b 100644
--- a/devlib/target.py
+++ b/devlib/target.py
@@ -102,6 +102,10 @@
         return None
 
     @property
+    def supported_abi(self):
+        return [self.abi]
+
+    @property
     @memoized
     def cpuinfo(self):
         return Cpuinfo(self.execute('cat /proc/cpuinfo'))
@@ -351,6 +355,38 @@
             command = 'cd {} && {}'.format(in_directory, command)
         return self.execute(command, as_root=as_root, timeout=timeout)
 
+    def background_invoke(self, binary, args=None, in_directory=None,
+                          on_cpus=None, as_root=False):
+        """
+        Executes the specified binary as a background task under the
+        specified conditions.
+
+        :binary: binary to execute. Must be present and executable on the device.
+        :args: arguments to be passed to the binary. The can be either a list or
+               a string.
+        :in_directory:  execute the binary in the  specified directory. This must
+                        be an absolute path.
+        :on_cpus:  taskset the binary to these CPUs. This may be a single ``int`` (in which
+                   case, it will be interpreted as the mask), a list of ``ints``, in which
+                   case this will be interpreted as the list of cpus, or string, which
+                   will be interpreted as a comma-separated list of cpu ranges, e.g.
+                   ``"0,4-7"``.
+        :as_root: Specify whether the command should be run as root
+
+        :returns: the subprocess instance handling that command
+        """
+        command = binary
+        if args:
+            if isiterable(args):
+                args = ' '.join(args)
+            command = '{} {}'.format(command, args)
+        if on_cpus:
+            on_cpus = bitmask(on_cpus)
+            command = '{} taskset 0x{:x} {}'.format(self.busybox, on_cpus, command)
+        if in_directory:
+            command = 'cd {} && {}'.format(in_directory, command)
+        return self.background(command, as_root=as_root)
+
     def kick_off(self, command, as_root=False):
         raise NotImplementedError()
 
@@ -798,6 +834,30 @@
 
     @property
     @memoized
+    def supported_abi(self):
+        props = self.getprop()
+        result = [props['ro.product.cpu.abi']]
+        if 'ro.product.cpu.abi2' in props:
+            result.append(props['ro.product.cpu.abi2'])
+        if 'ro.product.cpu.abilist' in props:
+            for abi in props['ro.product.cpu.abilist'].split(','):
+                if abi not in result:
+                    result.append(abi)
+
+        mapped_result = []
+        for supported_abi in result:
+            for abi, architectures in ABI_MAP.iteritems():
+                found = False
+                if supported_abi in architectures and abi not in mapped_result:
+                    mapped_result.append(abi)
+                    found = True
+                    break
+            if not found and supported_abi not in mapped_result:
+                mapped_result.append(supported_abi)
+        return mapped_result
+
+    @property
+    @memoized
     def os_version(self):
         os_version = {}
         for k, v in self.getprop().iteritems():
@@ -877,8 +937,16 @@
             pass
         self._connected_as_root = None
 
-    def connect(self, timeout=10, check_boot_completed=True):  # pylint: disable=arguments-differ
+    def wait_boot_complete(self, timeout=10):
         start = time.time()
+        boot_completed = boolean(self.getprop('sys.boot_completed'))
+        while not boot_completed and timeout >= time.time() - start:
+            time.sleep(5)
+            boot_completed = boolean(self.getprop('sys.boot_completed'))
+        if not boot_completed:
+            raise TargetError('Connected but Android did not fully boot.')
+
+    def connect(self, timeout=10, check_boot_completed=True):  # pylint: disable=arguments-differ
         device = self.connection_settings.get('device')
         if device and ':' in device:
             # ADB does not automatically remove a network device from it's
@@ -890,12 +958,7 @@
         super(AndroidTarget, self).connect(timeout=timeout)
 
         if check_boot_completed:
-            boot_completed = boolean(self.getprop('sys.boot_completed'))
-            while not boot_completed and timeout >= time.time() - start:
-                time.sleep(5)
-                boot_completed = boolean(self.getprop('sys.boot_completed'))
-            if not boot_completed:
-                raise TargetError('Connected but Android did not fully boot.')
+            self.wait_boot_complete(timeout)
 
     def setup(self, executables=None):
         super(AndroidTarget, self).setup(executables)
@@ -1088,6 +1151,12 @@
     def clear_logcat(self):
         adb_command(self.adb_name, 'logcat -c', timeout=30)
 
+    def adb_kill_server(self, timeout=30):
+        adb_command(self.adb_name, 'kill-server', timeout)
+
+    def adb_wait_for_device(self, timeout=30):
+        adb_command(self.adb_name, 'wait-for-device', timeout)
+
     def adb_reboot_bootloader(self, timeout=30):
         adb_command(self.adb_name, 'reboot-bootloader', timeout)
 
diff --git a/devlib/utils/android.py b/devlib/utils/android.py
index d0aa652..45bff20 100644
--- a/devlib/utils/android.py
+++ b/devlib/utils/android.py
@@ -27,7 +27,7 @@
 from collections import defaultdict
 
 from devlib.exception import TargetError, HostError, DevlibError
-from devlib.utils.misc import check_output, which, memoized
+from devlib.utils.misc import check_output, which, memoized, ABI_MAP
 from devlib.utils.misc import escape_single_quotes, escape_double_quotes
 
 
@@ -124,6 +124,7 @@
         self.label = None
         self.version_name = None
         self.version_code = None
+        self.native_code = None
         self.parse(path)
 
     def parse(self, apk_path):
@@ -143,6 +144,19 @@
             elif line.startswith('launchable-activity:'):
                 match = self.name_regex.search(line)
                 self.activity = match.group('name')
+            elif line.startswith('native-code'):
+                apk_abis = [entry.strip() for entry in line.split(':')[1].split("'") if entry.strip()]
+                mapped_abis = []
+                for apk_abi in apk_abis:
+                    found = False
+                    for abi, architectures in ABI_MAP.iteritems():
+                        if apk_abi in architectures:
+                            mapped_abis.append(abi)
+                            found = True
+                            break
+                    if not found:
+                        mapped_abis.append(apk_abi)
+                self.native_code = mapped_abis
             else:
                 pass  # not interested
 
@@ -385,8 +399,9 @@
                 raise TargetError(message.format(re_search[0]))
             else:
                 message = 'adb has returned early; did not get an exit code. '\
-                          'Was kill-server invoked?'
-                raise TargetError(message)
+                          'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\
+                          '-----\nERROR:\n-----\n{}\n-----'
+                raise TargetError(message.format(raw_output, error))
 
     return output
 
diff --git a/devlib/utils/gem5.py b/devlib/utils/gem5.py
new file mode 100644
index 0000000..c609d70
--- /dev/null
+++ b/devlib/utils/gem5.py
@@ -0,0 +1,43 @@
+#    Copyright 2017 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import re
+
+
+GEM5STATS_FIELD_REGEX = re.compile("^(?P<key>[^- ]\S*) +(?P<value>[^#]+).+$")
+GEM5STATS_DUMP_HEAD = '---------- Begin Simulation Statistics ----------'
+GEM5STATS_DUMP_TAIL = '---------- End Simulation Statistics   ----------'
+GEM5STATS_ROI_NUMBER = 8
+
+
+def iter_statistics_dump(stats_file):
+    '''
+    Yields statistics dumps as dicts. The parameter is assumed to be a stream 
+    reading from the statistics log file.
+    '''
+    cur_dump = {}
+    while True:
+        line = stats_file.readline()
+        if not line:
+            break
+        if GEM5STATS_DUMP_TAIL in line:
+            yield cur_dump
+            cur_dump = {}
+        else:
+            res = GEM5STATS_FIELD_REGEX.match(line) 
+            if res:
+                k = res.group("key")
+                v = res.group("value").split()
+                cur_dump[k] = v[0] if len(v)==1 else set(v)
+
diff --git a/devlib/utils/misc.py b/devlib/utils/misc.py
index b8626aa..f601686 100644
--- a/devlib/utils/misc.py
+++ b/devlib/utils/misc.py
@@ -41,7 +41,7 @@
 
 # ABI --> architectures list
 ABI_MAP = {
-    'armeabi': ['armeabi', 'armv7', 'armv7l', 'armv7el', 'armv7lh'],
+    'armeabi': ['armeabi', 'armv7', 'armv7l', 'armv7el', 'armv7lh', 'armeabi-v7a'],
     'arm64': ['arm64', 'armv8', 'arm64-v8a', 'aarch64'],
 }
 
@@ -79,9 +79,19 @@
         0xd08: {None: 'A72'},
         0xd09: {None: 'A73'},
     },
+    0x42: {  # Broadcom
+        0x516: {None: 'Vulcan'},
+    },
+    0x43: {  # Cavium
+        0x0a1: {None: 'Thunderx'},
+        0x0a2: {None: 'Thunderx81xx'},
+    },
     0x4e: {  # Nvidia
         0x0: {None: 'Denver'},
     },
+    0x50: {  # AppliedMicro
+        0x0: {None: 'xgene'},
+    },
     0x51: {  # Qualcomm
         0x02d: {None: 'Scorpion'},
         0x04d: {None: 'MSM8960'},
@@ -91,6 +101,7 @@
         },
         0x205: {0x1: 'KryoSilver'},
         0x211: {0x1: 'KryoGold'},
+        0x800: {None: 'Falkor'},
     },
     0x56: {  # Marvell
         0x131: {
diff --git a/doc/connection.rst b/doc/connection.rst
index 1d8f098..70d9e25 100644
--- a/doc/connection.rst
+++ b/doc/connection.rst
@@ -99,7 +99,7 @@
     ``adb`` is part of the Android SDK (though stand-alone versions are also
     available).
 
-    :param device: The name of the adb divice. This is usually a unique hex
+    :param device: The name of the adb device. This is usually a unique hex
                    string for USB-connected devices, or an ip address/port
                    combination. To see connected devices, you can run ``adb
                    devices`` on the host.
@@ -126,21 +126,21 @@
                     .. note:: ``keyfile`` and ``password`` can't be specified
                               at the same time.
 
-    :param port: TCP port on which SSH server is litening on the remoted device.
+    :param port: TCP port on which SSH server is listening on the remote device.
                  Omit to use the default port.
     :param timeout: Timeout for the connection in seconds. If a connection
                     cannot be established within this time, an error will be
                     raised.
     :param password_prompt: A string with the password prompt used by
                             ``sshpass``. Set this if your version of ``sshpass``
-                            uses somethin other than ``"[sudo] password"``.
+                            uses something other than ``"[sudo] password"``.
 
 
 .. class:: TelnetConnection(host, username, password=None, port=None,\
                             timeout=None, password_prompt=None,\
                             original_prompt=None)
 
-    A connectioned to a device on the network over Telenet.
+    A connection to a device on the network over Telenet.
 
     .. note:: Since Telenet protocol is does not support file transfer, scp is
               used for that purpose.
@@ -153,7 +153,7 @@
                                ``sshpass`` utility must be installed on the
                                system.
 
-    :param port: TCP port on which SSH server is litening on the remoted device.
+    :param port: TCP port on which SSH server is listening on the remote device.
                  Omit to use the default port.
     :param timeout: Timeout for the connection in seconds. If a connection
                     cannot be established within this time, an error will be
diff --git a/doc/instrumentation.rst b/doc/instrumentation.rst
index 7beec79..3c777ac 100644
--- a/doc/instrumentation.rst
+++ b/doc/instrumentation.rst
@@ -114,14 +114,14 @@
    :class:`Measurement` objects (one for each active channel).
 
    .. note:: This method is only implemented by :class:`Instrument`\ s that
-             support ``INSTANTANEOUS`` measurment.
+             support ``INSTANTANEOUS`` measurement.
 
 .. method:: Instrument.start()
 
    Starts collecting measurements from ``active_channels``.
 
    .. note:: This method is only implemented by :class:`Instrument`\ s that
-             support ``CONTINUOUS`` measurment.
+             support ``CONTINUOUS`` measurement.
 
 .. method:: Instrument.stop()
 
@@ -129,14 +129,14 @@
    :func:`start()`.
 
    .. note:: This method is only implemented by :class:`Instrument`\ s that
-             support ``CONTINUOUS`` measurment.
+             support ``CONTINUOUS`` measurement.
 
 .. method:: Instrument.get_data(outfile)
 
    Write collected data into ``outfile``. Must be called after :func:`stop()`.
    Data will be written in CSV format with a column for each channel and a row
    for each sample. Column heading will be channel, labels in the form
-   ``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the coluns
+   ``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the columns
    will be the same as the order of channels in ``Instrument.active_channels``.
 
    This returns a :class:`MeasurementCsv` instance associated with the outfile
@@ -144,7 +144,7 @@
    returned by ``take_measurement()``.
 
    .. note:: This method is only implemented by :class:`Instrument`\ s that
-             support ``CONTINUOUS`` measurment.
+             support ``CONTINUOUS`` measurement.
 
 .. attribute:: Instrument.sample_rate_hz
 
@@ -163,16 +163,16 @@
    ``site`` and a ``measurement_type``.
 
    A ``site`` indicates where  on the target a measurement is collected from
-   (e.g. a volage rail or location of a sensor).
+   (e.g. a voltage rail or location of a sensor).
 
    A ``measurement_type`` is an instance of :class:`MeasurmentType` that
-   describes what sort of measurment this is (power, temperature, etc). Each
-   mesurement type has a standard unit it is reported in, regardless of an
+   describes what sort of measurement this is (power, temperature, etc). Each
+   measurement type has a standard unit it is reported in, regardless of an
    instrument used to collect it.
 
    A channel (i.e. site/measurement_type combination) is unique per instrument,
    however there may be more than one channel associated with one site (e.g. for
-   both volatage and power).
+   both voltage and power).
 
    It should not be assumed that any site/measurement_type combination is valid.
    The list of available channels can queried with
@@ -180,22 +180,22 @@
 
 .. attribute:: InstrumentChannel.site
 
-   The name of the "site" from which the measurments are collected (e.g. voltage
+   The name of the "site" from which the measurements are collected (e.g. voltage
    rail, sensor, etc).
 
 .. attribute:: InstrumentChannel.kind
 
-   A string indingcating the type of measrument that will be collted. This is
+   A string indicating the type of measurement that will be collected. This is
    the ``name`` of the :class:`MeasurmentType` associated with this channel.
 
 .. attribute:: InstrumentChannel.units
 
-   Units in which measurment will be reported. this is determined by the
+   Units in which measurement will be reported. this is determined by the
    underlying :class:`MeasurmentType`.
 
 .. attribute:: InstrumentChannel.label
 
-   A label that can be attached to measurments associated with with channel.
+   A label that can be attached to measurements associated with with channel.
    This is constructed with ::
 
        '{}_{}'.format(self.site, self.kind)
diff --git a/doc/modules.rst b/doc/modules.rst
index ac75f99..b89b488 100644
--- a/doc/modules.rst
+++ b/doc/modules.rst
@@ -72,7 +72,7 @@
 
    :param cpu: The cpu; could be a numeric or the corresponding string (e.g.
         ``1`` or ``"cpu1"``).
-   :param governor: The name of the governor. This must be one of the governors 
+   :param governor: The name of the governor. This must be one of the governors
                 supported by the CPU (as returned by ``list_governors()``.
 
    Keyword arguments may be used to specify governor tunable values.
@@ -126,7 +126,7 @@
 cpuidle
 -------
 
-``cpufreq`` is the kernel subsystem for managing CPU low power (idle) states.
+``cpuidle`` is the kernel subsystem for managing CPU low power (idle) states.
 
 .. method:: target.cpuidle.get_driver()
 
@@ -155,7 +155,7 @@
     Enable or disable the specified or all states (optionally on the specified
     CPU.
 
-You can also call ``enable()`` or ``disable()`` on :class:`CpuidleState` objects 
+You can also call ``enable()`` or ``disable()`` on :class:`CpuidleState` objects
 returned by get_state(s).
 
 cgroups
@@ -182,7 +182,7 @@
 define the following class attributes:
 
 :name: A unique name for the module. This cannot clash with any of the existing
-       names and must be a valid Python identifier, but is otherwise free-from.
+       names and must be a valid Python identifier, but is otherwise free-form.
 :kind: This identifies the type of functionality a module implements, which in
        turn determines the interface implemented by the module (all modules of
        the same kind must expose a consistent interface). This must be a valid
@@ -271,7 +271,7 @@
 .. method:: HardResetModule.__call__()
 
     Must be implemented by derived classes.
-    
+
     Implements hard reset for a target devices. The equivalent of physically
     power cycling the device.  This may be used by client code in situations
     where the target becomes unresponsive and/or a regular reboot is not
@@ -355,7 +355,7 @@
         name = 'acme_hard_reset'
 
         def __call__(self):
-            # Assuming Acme board comes with a "reset-acme-board" utility 
+            # Assuming Acme board comes with a "reset-acme-board" utility
             os.system('reset-acme-board {}'.format(self.target.name))
 
     register_module(AcmeHardReset)
diff --git a/doc/overview.rst b/doc/overview.rst
index 421f053..7a60fb8 100644
--- a/doc/overview.rst
+++ b/doc/overview.rst
@@ -74,13 +74,13 @@
 working directories, deploying busybox, etc. It's usually enough to do this once
 for a new device, as the changes this makes will persist across reboots.
 However, there is no issue with calling this multiple times, so, to be on the
-safe site, it's a good idea to call this once at the beginning of your scripts.
+safe side, it's a good idea to call this once at the beginning of your scripts.
 
 Command Execution
 ~~~~~~~~~~~~~~~~~
 
 There are several ways to execute a command on the target. In each case, a
-:class:`TargetError` will be raised if something goes wrong. In very case, it is
+:class:`TargetError` will be raised if something goes wrong. In each case, it is
 also possible to specify ``as_root=True`` if the specified command should be
 executed as root.
 
@@ -154,7 +154,7 @@
    # kill all running instances of a process.
    t.killall('badexe', signal=signal.SIGKILL)
 
-   # List processes running on the target. This retruns a list of parsed
+   # List processes running on the target. This returns a list of parsed
    # PsEntry records.
    entries = t.ps()
    # e.g.  print virtual memory sizes of all running sshd processes:
diff --git a/doc/platform.rst b/doc/platform.rst
index 5270c09..3449db3 100644
--- a/doc/platform.rst
+++ b/doc/platform.rst
@@ -18,7 +18,7 @@
     :param core_names: A list of CPU core names in the order they appear
                        registered with the OS. If they are not specified,
                        they will be queried at run time.
-    :param core_clusters: Alist with cluster ids of each core (starting with
+    :param core_clusters: A list with cluster ids of each core (starting with
                           0). If this is not specified, clusters will be
                           inferred from core names (cores with the same name are
                           assumed to be in a cluster).
@@ -38,13 +38,13 @@
 The generic platform may be extended to support hardware- or
 infrastructure-specific functionality. Platforms exist for ARM
 VersatileExpress-based :class:`Juno` and :class:`TC2` development boards. In
-addition to the standard :class:`Platform` parameters above, these platfroms
+addition to the standard :class:`Platform` parameters above, these platforms
 support additional configuration:
 
 
 .. class:: VersatileExpressPlatform
 
-    Normally, this would be instatiated via one of its derived classes
+    Normally, this would be instantiated via one of its derived classes
     (:class:`Juno` or :class:`TC2`) that set appropriate defaults for some of
     the parameters.
 
@@ -63,7 +63,7 @@
                         mounted on the host system.
     :param hard_reset_method: Specifies the method for hard-resetting the devices
                             (e.g. if it becomes unresponsive and normal reboot
-                            method doesn not work). Currently supported methods
+                            method doesn't not work). Currently supported methods
                             are:
 
                             :dtr: reboot by toggling DTR line on the serial
@@ -80,7 +80,7 @@
                       The following values are currently supported:
 
                        :uefi: Boot via UEFI menu, by selecting the entry
-                              specified by ``uefi_entry`` paramter. If this
+                              specified by ``uefi_entry`` parameter. If this
                               entry does not exist, it will be automatically
                               created based on values provided for ``image``,
                               ``initrd``, ``fdt``, and ``bootargs`` parameters.
diff --git a/doc/target.rst b/doc/target.rst
index d14942e..b64246a 100644
--- a/doc/target.rst
+++ b/doc/target.rst
@@ -38,7 +38,7 @@
         by the connection's account). This location will be created, 
         if necessary, during ``setup()``.
 
-        This location does *not* to be same as the system's executables
+        This location does *not* need to be same as the system's executables
         location. In fact, to prevent devlib from overwriting system's defaults,
         it better if this is a separate location, if possible.
 
@@ -83,12 +83,12 @@
 
 .. attribute:: Target.big_core
 
-   This is the name of the cores that the "big"s in an ARM big.LITTLE
+   This is the name of the cores that are the "big"s in an ARM big.LITTLE
    configuration. This is obtained via the underlying :class:`Platform`.
 
 .. attribute:: Target.little_core
 
-   This is the name of the cores that the "little"s in an ARM big.LITTLE
+   This is the name of the cores that are the "little"s in an ARM big.LITTLE
    configuration. This is obtained via the underlying :class:`Platform`.
 
 .. attribute:: Target.is_connected
@@ -265,6 +265,24 @@
    :param timeout: If this is specified and invocation does not terminate within this number 
            of seconds, an exception will be raised.
 
+.. method:: Target.background_invoke(binary [, args [, in_directory [, on_cpus [, as_root ]]]])
+
+   Execute the specified binary on target (must already be installed) as a background
+   task, under the specified conditions and return the :class:`subprocess.Popen`
+   instance for the command.
+
+   :param binary: binary to execute. Must be present and executable on the device.
+   :param args: arguments to be passed to the binary. The can be either a list or
+          a string.
+   :param in_directory:  execute the binary in the  specified directory. This must
+                   be an absolute path.
+   :param on_cpus:  taskset the binary to these CPUs. This may be a single ``int`` (in which
+          case, it will be interpreted as the mask), a list of ``ints``, in which
+          case this will be interpreted as the list of cpus, or string, which
+          will be interpreted as a comma-separated list of cpu ranges, e.g.
+          ``"0,4-7"``.
+   :param as_root: Specify whether the command should be run as root
+
 .. method:: Target.kick_off(command [, as_root])
 
    Kick off the specified command on the target and return immediately. Unlike
@@ -422,12 +440,12 @@
 
 .. method:: Target.extract(path, dest=None)
 
-   Extracts the specified archive/file and returns the path to the extrated
+   Extracts the specified archive/file and returns the path to the extracted
    contents. The extraction method is determined based on the file extension.
    ``zip``, ``tar``, ``gzip``, and ``bzip2`` are supported.
 
    :param dest: Specified an on-target destination directory (which must exist)
-                for the extrated contents.
+                for the extracted contents.
 
     Returns the path to the extracted contents. In case of files (gzip and
     bzip2), the path to the decompressed file is returned; for archives, the