[LNT] r263559 - [profile] Add the initial scaffold for the LNT profiling infrastructure

James Molloy via llvm-commits llvm-commits at lists.llvm.org
Tue Mar 15 09:52:04 PDT 2016


Author: jamesm
Date: Tue Mar 15 11:52:04 2016
New Revision: 263559

URL: http://llvm.org/viewvc/llvm-project?rev=263559&view=rev
Log:
[profile] Add the initial scaffold for the LNT profiling infrastructure

This adds the API that will be used to query profiles in LNT's web stack along with a dumb implementation.

The dumb implementation serves as a way to easily import profile data (from whatever profile source). The auto-upgrade functionality in the profile API will then allow that to be upgraded to a more complex, more compact profile representation to be added later.

Added:
    lnt/trunk/lnt/testing/profile/
    lnt/trunk/lnt/testing/profile/__init__.py
    lnt/trunk/lnt/testing/profile/profile.py
    lnt/trunk/lnt/testing/profile/profilev1impl.py
    lnt/trunk/tests/testing/profilev1impl.py

Added: lnt/trunk/lnt/testing/profile/__init__.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/testing/profile/__init__.py?rev=263559&view=auto
==============================================================================
--- lnt/trunk/lnt/testing/profile/__init__.py (added)
+++ lnt/trunk/lnt/testing/profile/__init__.py Tue Mar 15 11:52:04 2016
@@ -0,0 +1,4 @@
+# This is the profile implementation registry. Register new profile implementations here.
+
+from profilev1impl import ProfileV1
+IMPLEMENTATIONS = {1: ProfileV1}

Added: lnt/trunk/lnt/testing/profile/profile.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/testing/profile/profile.py?rev=263559&view=auto
==============================================================================
--- lnt/trunk/lnt/testing/profile/profile.py (added)
+++ lnt/trunk/lnt/testing/profile/profile.py Tue Mar 15 11:52:04 2016
@@ -0,0 +1,206 @@
+import os, tempfile, base64
+
+class Profile(object):
+    """Profile objects hold a performance profile.
+
+    The Profile class itself is a thin wrapper around a ProfileImpl
+    object, which is what actually holds, reads, writes and dispenses
+    the profile information.
+    """
+    # Import this late to avoid a cyclic dependency
+    import lnt.testing.profile
+    
+    def __init__(self, impl):
+        """Internal constructor. Users should not call this; use fromFile or
+        fromRendered."""
+        assert isinstance(impl, ProfileImpl)
+        self.impl = impl
+
+    @staticmethod
+    def fromFile(f):
+        """
+        Load a profile from a file.
+        """
+        for impl in lnt.testing.profile.IMPLEMENTATIONS.values():
+            if impl.checkFile(f):
+                ret = impl.deserialize(open(f, 'rb'))
+                if ret:
+                    return Profile(ret)
+                else:
+                    return None
+        raise RuntimeError('No profile implementations could read this file!')
+
+    @staticmethod
+    def fromRendered(s):
+        """
+        Load a profile from a string, which must have been produced
+        with Profile.render(). The format of this is not the same as the
+        on-disk format; it is base64 encoded to survive wire transfer.
+        """
+        s = base64.b64decode(s)
+        with tempfile.NamedTemporaryFile() as fd:
+            fd.write(s)
+            # Rewind to beginning.
+            fd.flush()
+            fd.seek(0)
+            
+            for impl in lnt.testing.profile.IMPLEMENTATIONS.values():
+                if impl.checkFile(fd.name):
+                    ret = impl.deserialize(fd)
+                    if ret:
+                        return Profile(ret)
+                    else:
+                        return None
+        raise RuntimeError('No profile implementations could read this file!')
+
+    def save(self, filename=None, profileDir=None, prefix=''):
+        """
+        Save a profile. One of 'filename' or 'profileDir' must be given.
+          - If 'filename' is given, that is where the profile is saved.
+          - If 'profileDir' is given, a new unique filename is created
+            inside 'profileDir', optionally with 'prefix'.
+
+        The filename written to is returned.
+        """
+        if filename:
+            self.impl.serialize(filename)
+            return filename
+
+        assert profileDir is not None
+        if not os.path.exists(profileDir):
+            os.makedirs(profileDir)
+        tf = tempfile.NamedTemporaryFile(prefix=prefix,
+                                         suffix='.lntprof',
+                                         dir=profileDir,
+                                         delete=False)
+        self.impl.serialize(tf.name)
+
+        # FIXME: make the returned filepath relative to baseDir?
+        return tf.name
+
+    def render(self):
+        """
+        Return a string representing this profile suitable for storing inside a
+        JSON object.
+
+        Implementation note: the string is base64 encoded.
+        """
+        return base64.b64encode(self.impl.serialize())
+
+    def upgrade(self):
+        """
+        Upgrade to the latest implementation version.
+
+        Returns self.
+        """
+        while True:
+            version = self.impl.getVersion()
+            new_version = version + 1
+            if new_version not in lnt.testing.profile.IMPLEMENTATIONS:
+                return self
+            self.impl = lnt.testing.profile.IMPLEMENTATIONS[new_version].upgrade(self.impl)
+
+    #
+    # ProfileImpl facade - see ProfileImpl documentation below.
+    #
+
+    def getVersion(self):
+        return self.impl.getVersion()
+    
+    def getTopLevelCounters(self):
+        return self.impl.getTopLevelCounters()
+
+    def getDisassemblyFormat(self):
+        return self.impl.getDisassemblyFormat()
+    
+    def getFunctions(self):
+        return self.impl.getFunctions()
+
+    def getCodeForFunction(self, fname):
+        return self.impl.getCodeForFunction(fname)
+
+################################################################################
+
+class ProfileImpl(object):
+    @staticmethod
+    def upgrade(old):
+        """
+        Takes a previous profile implementation in 'old' and returns a new ProfileImpl
+        for this version. The only old version that must be supported is the immediately
+        prior version (e.g. version 3 only has to handle upgrades from version 2.
+        """
+        raise NotImplementedError("Abstract class")
+
+    @staticmethod
+    def checkFile(fname):
+        """
+        Return True if 'fname' is a serialized version of this profile implementation.
+        """
+        raise NotImplementedError("Abstract class")
+    
+    @staticmethod
+    def deserialize(fobj):
+        """
+        Reads a profile from 'fobj', returning a new profile object. This can be lazy.
+        """
+        raise NotImplementedError("Abstract class")
+
+    def serialize(self, fname=None):
+        """
+        Serializes the profile to the given filename (base). If fname is None, returns
+        as a bytes instance.
+        """
+        raise NotImplementedError("Abstract class")
+
+    def getVersion(self):
+        """
+        Return the profile version.
+        """
+        raise NotImplementedError("Abstract class")
+
+    def getTopLevelCounters(self):
+        """
+        Return a dict containing the counters for the entire profile. These will
+        be absolute numbers: {'cycles': 5000.0} for example.
+        """
+        raise NotImplementedError("Abstract class")
+
+    def getDisassemblyFormat(self):
+        """
+        Return the format for the disassembly strings returned by getCodeForFunction().
+        Possible values are:
+          'raw'                   - No interpretation available - pure strings.
+          'marked-up-disassembly' - LLVM marked up disassembly format.
+        """
+        raise NotImplementedError("Abstract class")
+    
+    def getFunctions(self):
+        """
+        Return a dict containing function names to information about that function.
+
+        The information dict contains:
+          - 'counters' - counter values for the function.
+          - 'length' - number of times to call getCodeForFunction to obtain all
+                       instructions.
+
+        The dict should *not* contain disassembly / function contents.
+        The counter values must be percentages, not absolute numbers.
+
+        E.g. {'main': {'counters': {'cycles': 50.0, 'branch-misses': 0}, 'length': 200},
+              'dotest': {'counters': {'cycles': 50.0, 'branch-misses': 0}, 'length': 4}}
+        """
+        raise NotImplementedError("Abstract class")
+
+    def getCodeForFunction(self, fname):
+        """
+        Return a *generator* which will return, for every invocation, a three-tuple:
+
+          (counters, address, text)
+
+        Where counters is a dict : (e.g.) {'cycles': 50.0}, text is in the
+        format as returned by getDisassemblyFormat(), and address is an integer.
+
+        The counter values must be percentages (of the function total), not
+        absolute numbers.
+        """
+        raise NotImplementedError("Abstract class")

Added: lnt/trunk/lnt/testing/profile/profilev1impl.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/lnt/testing/profile/profilev1impl.py?rev=263559&view=auto
==============================================================================
--- lnt/trunk/lnt/testing/profile/profilev1impl.py (added)
+++ lnt/trunk/lnt/testing/profile/profilev1impl.py Tue Mar 15 11:52:04 2016
@@ -0,0 +1,81 @@
+from lnt.testing.profile.profile import ProfileImpl
+import cPickle, zlib
+
+class ProfileV1(ProfileImpl):
+    """
+ProfileV1 files not clever in any way. They are simple Python objects with
+the profile data layed out in the most obvious way for production/consumption
+that are then pickled and compressed.
+
+They are expected to be created by simply storing into the self.data member.
+
+The self.data member has this format:
+
+{
+ counters: {'cycles': 12345.0, 'branch-misses': 200.0}, # Counter values are absolute.
+ disassembly-format: 'raw',
+ functions: {
+   name: {
+     counters: {'cycles': 45.0, ...}, # Note counters are now percentages.
+     data: [
+       [463464, {'cycles': 23.0, ...}, '\tadd r0, r0, r1'}],
+       ...
+     ]
+   }
+  }
+}
+    """
+
+    def __init__(self, data):
+        """
+        Create from a raw data dict. data has the format given in the class docstring.
+        """
+        self.data = data
+    
+    @staticmethod
+    def upgrade(old):
+        raise RuntimeError("Cannot upgrade to version 1!")
+
+    @staticmethod
+    def checkFile(fn):
+        # "zlib compressed data" - 78 9C
+        return open(fn).read(2) == '\x78\x9c'
+
+    @staticmethod
+    def deserialize(fobj):
+        o = zlib.decompress(fobj.read())
+        data = cPickle.loads(o)
+        return ProfileV1(data)
+
+    def serialize(self, fname=None):
+        obj = cPickle.dumps(self.data)
+        compressed_obj = zlib.compress(obj)
+
+        if fname is None:
+            return bytes(compressed_obj)
+        else:
+            with open(fname, 'w') as fd:
+                fd.write(compressed_obj)
+
+    def getVersion(self):
+        return 1
+
+    def getTopLevelCounters(self):
+        return self.data['counters']
+
+    def getDisassemblyFormat(self):
+        if 'disassembly-format' in self.data:
+            return self.data['disassembly-format']
+        return 'raw'
+    
+    def getFunctions(self):
+        d = {}
+        for fn in self.data['functions']:
+            f = self.data['functions'][fn]
+            d[fn] = dict(counters=f.get('counters', {}),
+                         length=len(f.get('data', [])))
+        return d
+
+    def getCodeForFunction(self, fname):
+        for l in self.data['functions'][fname].get('data', []):
+            yield (l[0], l[1], l[2])

Added: lnt/trunk/tests/testing/profilev1impl.py
URL: http://llvm.org/viewvc/llvm-project/lnt/trunk/tests/testing/profilev1impl.py?rev=263559&view=auto
==============================================================================
--- lnt/trunk/tests/testing/profilev1impl.py (added)
+++ lnt/trunk/tests/testing/profilev1impl.py Tue Mar 15 11:52:04 2016
@@ -0,0 +1,45 @@
+# RUN: python %s
+import unittest, logging, sys, copy, tempfile, io
+from lnt.testing.profile.profilev1impl import ProfileV1
+
+
+logging.basicConfig(level=logging.DEBUG)
+
+class ProfileV1Test(unittest.TestCase):
+    def setUp(self):
+        self.test_data = {
+            'counters': {'cycles': 12345.0, 'branch-misses': 200.0},
+            'disassembly-format': 'raw',
+            'functions': {
+                'fn1': {
+                    'counters': {'cycles': 45.0, 'branch-misses': 10.0},
+                    'data': [
+                        [{}, 0x100000, 'add r0, r0, r0'],
+                        [{'cycles': 100.0}, 0x100004, 'sub r1, r0, r0']
+                    ]
+                }
+            }
+        }
+
+    def test_serialize(self):
+        p = ProfileV1(copy.deepcopy(self.test_data))
+        with tempfile.NamedTemporaryFile() as f:
+            s = p.serialize(f.name)
+            self.assertTrue(ProfileV1.checkFile(f.name))
+
+    def test_deserialize(self):
+        p = ProfileV1(copy.deepcopy(self.test_data))
+        s = p.serialize()
+        fobj = io.BytesIO(s)
+        p2 = ProfileV1.deserialize(fobj)
+
+        self.assertEqual(p2.data, self.test_data)
+
+    def test_getFunctions(self):
+        p = ProfileV1(copy.deepcopy(self.test_data))
+        self.assertEqual(p.getFunctions(),
+                         {'fn1': {'counters': {'cycles': 45.0, 'branch-misses': 10.0},
+                                  'length': 2}})
+
+if __name__ == '__main__':
+    unittest.main(argv=[sys.argv[0], ])




More information about the llvm-commits mailing list