diff --git a/devlib/trace/systrace.py b/devlib/trace/systrace.py
new file mode 100644
index 0000000..7c72549
--- /dev/null
+++ b/devlib/trace/systrace.py
@@ -0,0 +1,158 @@
+#    Copyright 2018 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 os
+import subprocess
+
+from shutil import copyfile
+from tempfile import NamedTemporaryFile
+
+from devlib.exception import TargetError, HostError
+from devlib.trace import TraceCollector
+from devlib.utils.android import platform_tools
+from devlib.utils.misc import memoized
+
+
+DEFAULT_CATEGORIES = [
+    'gfx',
+    'view',
+    'sched',
+    'freq',
+    'idle'
+]
+
+class SystraceCollector(TraceCollector):
+    """
+    A trace collector based on Systrace
+
+    For more details, see https://developer.android.com/studio/command-line/systrace
+
+    :param target: Devlib target
+    :type target: AndroidTarget
+
+    :param outdir: Working directory to use on the host
+    :type outdir: str
+
+    :param categories: Systrace categories to trace. See `available_categories`
+    :type categories: list(str)
+
+    :param buffer_size: Buffer size in kb
+    :type buffer_size: int
+
+    :param strict: Raise an exception if any of the requested categories
+        are not available
+    :type strict: bool
+    """
+
+    @property
+    @memoized
+    def available_categories(self):
+        lines = subprocess.check_output([self.systrace_binary, '-l']).splitlines()
+
+        categories = []
+        for line in lines:
+            categories.append(line.split()[0])
+
+        return categories
+
+    def __init__(self, target,
+                 categories=None,
+                 buffer_size=None,
+                 strict=False):
+
+        super(SystraceCollector, self).__init__(target)
+
+        self.categories = categories or DEFAULT_CATEGORIES
+        self.buffer_size = buffer_size
+
+        self._systrace_process = None
+        self._tmpfile = None
+
+        # Try to find a systrace binary
+        self.systrace_binary = None
+
+        systrace_binary_path = os.path.join(platform_tools, 'systrace', 'systrace.py')
+        if not os.path.isfile(systrace_binary_path):
+            raise HostError('Could not find any systrace binary under {}'.format(platform_tools))
+
+        self.systrace_binary = systrace_binary_path
+
+        # Filter the requested categories
+        for category in self.categories:
+            if category not in self.available_categories:
+                message = 'Category [{}] not available for tracing'.format(category)
+                if strict:
+                    raise TargetError(message)
+                self.logger.warning(message)
+
+        self.categories = list(set(self.categories) & set(self.available_categories))
+        if not self.categories:
+            raise TargetError('None of the requested categories are available')
+
+    def __del__(self):
+        self.reset()
+
+    def _build_cmd(self):
+        self._tmpfile = NamedTemporaryFile()
+
+        self.systrace_cmd = '{} -o {} -e {}'.format(
+            self.systrace_binary,
+            self._tmpfile.name,
+            self.target.adb_name
+        )
+
+        if self.buffer_size:
+            self.systrace_cmd += ' -b {}'.format(self.buffer_size)
+
+        self.systrace_cmd += ' {}'.format(' '.join(self.categories))
+
+    def reset(self):
+        if self._systrace_process:
+            self.stop()
+
+        if self._tmpfile:
+            self._tmpfile.close()
+            self._tmpfile = None
+
+    def start(self):
+        if self._systrace_process:
+            raise RuntimeError("Tracing is already underway, call stop() first")
+
+        self.reset()
+
+        self._build_cmd()
+
+        self._systrace_process = subprocess.Popen(
+            self.systrace_cmd,
+            stdin=subprocess.PIPE,
+            shell=True
+        )
+
+    def stop(self):
+        if not self._systrace_process:
+            raise RuntimeError("No tracing to stop, call start() first")
+
+        # Systrace expects <enter> to stop
+        self._systrace_process.communicate('\n')
+        self._systrace_process = None
+
+    def get_trace(self, outfile):
+        if self._systrace_process:
+            raise RuntimeError("Tracing is underway, call stop() first")
+
+        if not self._tmpfile:
+            raise RuntimeError("No tracing data available")
+
+        copyfile(self._tmpfile.name, outfile)