[Zope-Checkins] CVS: Zope/lib/python/Products/CallProfiler - MethodWrapper.py:1.1.2.1 utilities.py:1.1.2.1 CallProfiler.py:1.1.2.2 TODO.txt:1.1.2.2 profiler.py:1.1.2.2

Anthony Baxter anthony@interlink.com.au
Mon, 20 May 2002 03:22:07 -0400


Update of /cvs-repository/Zope/lib/python/Products/CallProfiler
In directory cvs.zope.org:/tmp/cvs-serv2477

Modified Files:
      Tag: anthony-CallProfiler-branch
	CallProfiler.py TODO.txt profiler.py 
Added Files:
      Tag: anthony-CallProfiler-branch
	MethodWrapper.py utilities.py 
Log Message:
First commit of merged ZC changes (from Ken, Tres and Shane (according to
the rcs dollarLogdollar))

Major changes:

  You can now wrap/instrument _any_ method, not just the __call__ or _exec
  one. 

  Some changes to the way displays work.

  Various cleanups.

Note _well_ the second entry in the TODO.txt file: Right now, if the
publisher hook is installed, and you refresh this product, you will 
end up with a trashed Zope that needs a restart. I'm looking at this
now. I don't see exactly why this is happening, yet.

Still some display cleanup to come, as well.



=== Added File Zope/lib/python/Products/CallProfiler/MethodWrapper.py ===
"""Enclose a method in an instance whose call interjects profiling activity."""

from profiler import profiler
from MethodObject import Method
import Acquisition
from ComputedAttribute import ComputedAttribute


class MethodWrapper (Method):
    """Encapsulate and masquerade as a method, surrounding calls of the method
    with profiling."""

    IS_PROFILE_WRAPPED = 1

    def __init__(self, func):
        func = getattr(func, 'im_func', func)
        # Setting these attributes satisfies mapply.
        self.target_func_ = func
        self.func_code = func.func_code
        self.func_defaults = func.func_defaults
        self.im_func = func

    # 'Method' base class ensures that 'parent' will get passed on __call__().
    def __call__(self, parent, *args, **kw):
        path = ("/".join(parent.getPhysicalPath())
                + '/' + self.target_func_.__name__)

        profiler.startCall(parent.meta_type, path, parent.getId())
        try:
            return self.target_func_(parent, *args, **kw)

        finally:
            profiler.endCall()


=== Added File Zope/lib/python/Products/CallProfiler/utilities.py ===
"""Sundry items for the CallProfiler."""

from profiler import profiler
from MethodWrapper import MethodWrapper

def profiler_publish_hook(request, *args, **kw):
    """Instrument publisher for CallProfiler profiling."""

    profiler.startRequest(request)
    import ZPublisher.Publish
    try:
        return ZPublisher.Publish.profiler_publish_original(request,
                                                            *args, **kw)
    finally:
        # if we die here, we want to catch it or the publisher will get
        # confused...
        try:
            profiler.endRequest()
        except:
            # log the error though
            import sys
            LOG('CallProfiler.publish_hook', ERROR,
                'Error during endmark()', error=sys.exc_info())

default_marker = []
def profiler_cache_hook(self, view_name='', keywords=None, mtime_func=None,
        default=default_marker):
    '''A cache hook
    '''
    import OFS.Cache
    ret = self.profiler_cache_original(view_name, keywords, mtime_func,
                                       default)
    if ret is default:
        profiler.cacheMiss()
    else:
        profiler.cacheHit()
    return ret



=== Zope/lib/python/Products/CallProfiler/CallProfiler.py 1.1.2.1 => 1.1.2.2 ===
 from Globals import InitializeClass, HTMLFile
 from OFS.SimpleItem import Item
-from Acquisition import Implicit
+from Acquisition import Implicit, Explicit
+import ExtensionClass
 from Persistence import Persistent
 from AccessControl import ClassSecurityInfo
 from AccessControl import ModuleSecurityInfo
@@ -35,6 +36,8 @@
 
 # get the profiler store
 from profiler import profiler
+from MethodWrapper import MethodWrapper
+from utilities import *
 
 def profiler_call_hook(self, *args, **kw):
     '''A call hook
@@ -53,7 +56,9 @@
     profiler.startRequest(request)
     import ZPublisher.Publish
     try:
-        return ZPublisher.Publish.profiler_publish_original(request, *args, **kw)
+        return ZPublisher.Publish.profiler_publish_original(request, 
+                                                            *args, 
+                                                            **kw)
     finally:
         # if we die here, we want to catch it or the publisher will get
         # confused...
@@ -71,7 +76,8 @@
     '''A cache hook
     '''
     import OFS.Cache
-    ret = self.profiler_cache_original(view_name, keywords, mtime_func, default)
+    ret = self.profiler_cache_original(view_name, keywords, 
+                                       mtime_func, default)
     if ret is default:
         profiler.cacheMiss()
     else:
@@ -80,9 +86,10 @@
 
 
 class Profileable:
-    def __init__(self, module, klass, method):
+
+    def __init__(self, module, klass, method_name):
         self.module = module
-        self.method = method
+        self.method_name = method_name
 
         # get the actual class to patch
         try:
@@ -94,64 +101,49 @@
         for comp in components[1:]:
             mod = getattr(mod, comp)
         self.klass = getattr(mod, klass)
-        self.name = self.klass.meta_type
+        if hasattr(self.klass, 'meta_type'):
+            self.name = self.klass.meta_type
+        else:
+            self.name = self.klass.__name__
         self.icon = None
         if hasattr(self.klass, 'icon'):
             self.icon = self.klass.icon
 
     def install(self):
-        '''Install the call hook
+        '''Install the call hook if not already installed
         '''
-        self.klass.profiler_call_original = getattr(self.klass, self.method)
-        setattr(self.klass, self.method, profiler_call_hook)
+        if not self.isInstalled():
+            method = getattr(self.klass, self.method_name)
+            setattr(self.klass, self.method_name, MethodWrapper(method))
 
     def uninstall(self):
-        '''Uninstall the call hook
+        '''Uninstall the call hook if present
         '''
-        setattr(self.klass, self.method, self.klass.profiler_call_original)
-        del self.klass.profiler_call_original
+        if self.isInstalled():
+            method = getattr(self.klass, self.method_name)
+            setattr(self.klass, self.method_name, method.target_func_)
 
     def isInstalled(self):
-        '''See if the call hook has been installed
+        '''returns True if target method is already wrapped for profiling
         '''
-        return hasattr(self.klass, 'profiler_call_original')
+        method = getattr(self.klass, self.method_name, None)
+        return getattr(method, 'IS_PROFILE_WRAPPED', None)
 
     def isAvailable(self):
         '''See if the module is actually available
         '''
-        return self.klass is not None
+        return (self.klass is not None 
+            and hasattr(self.klass, self.method_name))
 
-    def checkbox(self):
-        '''Display a checkbox to configure this
-        '''
-        if self.isInstalled():
-            s = 'CHECKED'
-        else:
-            s = ''
-        return '<input name="enabled:list" type="checkbox" value="%s"%s>%s'%(
-            self.name, s, self.name)
-
-profileable_modules = {
-    'Page Template': Profileable('Products.PageTemplates.PageTemplates',
-        'PageTemplates', '__call__'),
-    'DTML Method': Profileable('OFS.DTMLMethod', 'DTMLMethod', '__call__'),
-    'MLDTMLMethod': Profileable('Products.MLDTML.MLDTML',
-        'MLDTMLMethod', '__call__'),
-    'Z SQL Method': Profileable('Products.ZSQLMethods.SQL', 'SQL', '__call__'),
-    'Python Method': Profileable('Products.PythonMethod.PythonMethod',
-        'PythonMethod', '__call__'),
-    'Script (Python)': Profileable('Products.PythonScripts.PythonScript',
-        'PythonScript', '_exec'),
-    'FSPythonScript':
-        Profileable('Products.CMFCore.FSPythonScript', 'FSPythonScript',
-            '__call__'),
-    'FSDTMLMethod':
-        Profileable('Products.CMFCore.FSDTMLMethod', 'FSDTMLMethod',
-            '__call__'),
-    'FSPageTemplate':
-        Profileable('Products.CMFCore.FSPageTemplate', 'FSPageTemplate',
-            '__call__'),
-}
+    def __repr__(self):
+        name = getattr(self.klass, '__name__',
+                       getattr(self.klass, 'meta_type',
+                               str(self.klass)))
+        if self.method_name: method_name = ".%s()" % self.method_name
+        else: method_name = "--"
+        return ("<%s instance %s%s at 0x%s>"
+                % (self.__class__.__name__, name, method_name,
+                   hex(id(self))[2:]))
 
 modulesecurity = ModuleSecurityInfo()
 
@@ -170,6 +162,39 @@
     self._setObject(id, c)
     return self.manage_main(self, REQUEST)
 
+profileable_modules = {}
+
+def addMethod(dotted_module, class_name, method_name):
+    """Add or replace an entry in the roster of profileable class methods.
+
+    Returns the Profileable instance representing the module."""
+    p = Profileable(dotted_module, class_name, method_name)
+    if p.isAvailable(): name = p.name
+    else:               name = class_name
+
+    registered = profileable_modules.setdefault(name, [])
+    for i in range(len(registered)):
+        if registered[i].method_name == method_name:
+            # Replace existing entry.
+            registered[i] = p
+            return p
+    # Add new entry.
+    registered.append(p)
+    return p
+
+addMethod('Products.PageTemplates.ZopePageTemplate', 'ZopePageTemplate',
+          '_exec')
+addMethod('Products.PageTemplates.PageTemplateFile', 'PageTemplateFile',
+          '__call__')
+addMethod('OFS.DTMLMethod', 'DTMLMethod', '__call__')
+addMethod('Products.MLDTML.MLDTML', 'MLDTMLMethod', '__call__')
+addMethod('Products.ZSQLMethods.SQL', 'SQL', '__call__')
+addMethod('Products.PythonMethod.PythonMethod', 'PythonMethod', '__call__')
+addMethod('Products.PythonScripts.PythonScript', 'PythonScript', '_exec')
+addMethod('Products.CMFCore.FSPythonScript', 'FSPythonScript', '__call__')
+addMethod('Products.CMFCore.FSDTMLMethod', 'FSDTMLMethod', '__call__')
+addMethod('Products.CMFCore.FSPageTemplate', 'FSPageTemplate', '__call__')
+
 class CallProfiler(Item, Implicit, Persistent):
     '''An instance of this class provides an interface between Zope and
        roundup for one roundup instance
@@ -200,51 +225,63 @@
 
     security.declareProtected('View management screens', 'getComponentModules')
     def getComponentModules(self):
-        '''List the components available to profile
+        '''List the components available for profiling
+
+        We return a list of tuples, consisting of:
+          - the module name
+          - the module's profileable_modules entry."""
         '''
         l = []
         names = profileable_modules.keys()
         names.sort()
         for name in names:
-            if profileable_modules[name].isAvailable():
-                l.append((name, profileable_modules[name]))
+            for profileable in profileable_modules[name]:
+                if profileable.isAvailable():
+                    l.append((name, profileable))
         return l
 
     security.declareProtected('View management screens', 'monitorAll')
     def monitorAll(self):
-        '''Set to monitor all that we can
-        '''
-        enabled = [x[0] for x in self.getComponentModules()]
+        '''Configure all available modules for monitoring.'''
+        enabled = ["%s.%s" % (x[1].name, x[1].method_name)
+                   for x in self.getComponentModules()]
         return self.configure(enabled=enabled)
 
     security.declareProtected('View management screens', 'monitorNone')
     def monitorNone(self):
-        '''Set to monitor no calls
+        '''Configure all available modules for not being monitored.
         '''
         return self.configure()
 
-    security.declareProtected('View management screens', 'addModule')
-    def addModule(self, dotted_module, class_name, method ):
-        '''Add / replace a module "on the fly".
+    security.declareProtected('View management screens', 'addMethod')
+    def addMethod(self, dotted_module, class_name, method):
+        '''Add / replace a method "on the fly".
         '''
-        p = Profileable(dotted_module, class_name, method)
-        profileable_modules[p.name] = p
+        p = addMethod(dotted_module, class_name, method)
         message = 'Module %s added.' % p.name
         return self.configureForm(self, self.REQUEST,
-            manage_tabs_message=message)
+                                  manage_tabs_message=message)
 
     security.declareProtected('View management screens', 'removeModule')
     def removeModule(self, name):
         '''Remove a module "on the fly".
+
+        We ensure that the profiling wrapper for all the module's registered
+        methods is not installed."""
         '''
-        del profileable_modules[name]
-        message = 'Module %s removed.' % name
+        splitname = name.split('.')
+        module, method_name = '.'.join(splitname[:-1]), splitname[-1]
+        for profileable in profileable_modules[module]:
+            if profileable.isInstalled():
+                profileable.uninstall()
+        del profileable_modules[module]
+        message = 'Module %s removed.' % module
         return self.configureForm(self, self.REQUEST,
-            manage_tabs_message=message)
+                                  manage_tabs_message=message)
 
     security.declareProtected('View management screens', 'configure')
     def configure(self, enabled=[]):
-        '''Set the given items to enabled
+        '''set the given modules to enabled or disabled.
         '''
         # install or uninstall the publisher hook as required
         if not enabled:
@@ -258,12 +295,21 @@
             if not self.isCacheHookInstalled():
                 self.installCacheHook()
 
-        # now install the selected modules
-        for component, module in self.getComponentModules():
-            if component in enabled and not module.isInstalled():
-                module.install()
-            elif component not in enabled and module.isInstalled():
-                module.uninstall()
+        # Now compose a data structure by which the modules can be configured:
+        settings = {}
+        for entry in enabled:
+            splitentry = entry.split('.')
+            module, method_name = '.'.join(splitentry[:-1]), splitentry[-1]
+            settings.setdefault(module, []).append(method_name)
+
+        # now implement indicated installations/deinstallations:
+        for name, profileable in self.getComponentModules():
+            settings_entry = settings.get(profileable.name, [])
+            if profileable.isInstalled():
+                if profileable.method_name not in settings_entry:
+                    profileable.uninstall()
+            elif profileable.method_name in settings_entry:
+                profileable.install()
 
         if not enabled:
             message = 'all profiling disabled'
@@ -271,7 +317,7 @@
             message = ', '.join(enabled) + ' enabled'
 
         return self.configureForm(self, self.REQUEST,
-            manage_tabs_message=message)
+                                  manage_tabs_message=message)
 
     # PUBLISHER hook
     security.declarePrivate('installPublisherHook')
@@ -298,7 +344,6 @@
         import ZPublisher.Publish
         return hasattr(ZPublisher.Publish, 'profiler_publish_original')
 
-
     # CACHE hook
     security.declarePrivate('installCacheHook')
     def installCacheHook(self):
@@ -381,9 +426,11 @@
             info['percentage_int'] = int(percent/2)
             info['icon'] = ''
             if info.has_key('meta_type'):
-                module = pm[info['meta_type']]
-                if module.icon:
-                    info['icon'] = module.icon
+                # use the first icon found
+                for module in pm[info['meta_type']]:
+                    if module.icon:
+                        info['icon'] = module.icon
+                        break
             if percent > 10: info['colour'] = '#ffbbbb'
             elif percent > 5: info['colour'] = '#ffdbb9'
             elif percent > 3: info['colour'] = '#fff9b9'
@@ -473,9 +520,11 @@
 
             info['icon'] = ''
             if info.has_key('meta_type'):
-                module = pm[info['meta_type']]
-                if module.icon:
-                    info['icon'] = module.icon
+                # Use the first icon found
+                for module in pm[info['meta_type']]:
+                    if module.icon:
+                        info['icon'] = module.icon
+                        break
 
             info['percentage_int'] = int(percent/2)
             if percent > 10: info['colour'] = '#ffbbbb'
@@ -491,6 +540,26 @@
 
 #
 # $Log$
+# Revision 1.1.2.2  2002/05/20 07:21:36  anthony
+# First commit of merged ZC changes (from Ken, Tres and Shane (according to
+# the rcs dollarLogdollar))
+#
+# Major changes:
+#
+#   You can now wrap/instrument _any_ method, not just the __call__ or _exec
+#   one.
+#
+#   Some changes to the way displays work.
+#
+#   Various cleanups.
+#
+# Note _well_ the second entry in the TODO.txt file: Right now, if the
+# publisher hook is installed, and you refresh this product, you will
+# end up with a trashed Zope that needs a restart. I'm looking at this
+# now. I don't see exactly why this is happening, yet.
+#
+# Still some display cleanup to come, as well.
+#
 # Revision 1.1.2.1  2002/05/17 05:26:53  anthony
 # Initial checkin of CallProfiler branch. This is the internal ekit version.
 #


=== Zope/lib/python/Products/CallProfiler/TODO.txt 1.1.2.1 => 1.1.2.2 ===
 
+. Don't in-line the detailed display when only one data set is being 
+  shown. But mark the entry that's being displayed.
+
+. if you refresh when the publisher hook's installed, you trash the 
+  publisher, and have to restart Zope. The publisher __call__ method 
+  gets replaced by None. 
+
+. when first loaded, the icons are not loaded. the product klass has an
+  'icon' attribute that's empty. I suspect that the check for icons is
+  happening before the products are loaded up. Should refresh them when
+  the Configure screen is hit.
+
 . determine the pecentages for highlighting dynamically
   - if there's < 5 calls, red is >50%
   - if there's < 10 calls, red is >20%?
@@ -10,11 +22,11 @@
 . allow auto-culling or auto-aggregation of results when it's left on for a
   long time
 
-. include a stringification of PARENTS and getPhysicalPath to indicate where
-  particular methods are
-
 
 DONE
+
+. include a stringification of PARENTS and getPhysicalPath to indicate where
+  particular methods are
 
 . sorting of columns in the results
 


=== Zope/lib/python/Products/CallProfiler/profiler.py 1.1.2.1 => 1.1.2.2 ===
             transaction.finish()
 
-    def startCall(self, type, name):
+    def startCall(self, type, path, name):
         '''Register the start of a call
         '''
         transaction = self.getTransaction()
         if transaction is None: return
-        transaction.startCall(type, name)
+        transaction.startCall(type, path, name)
 
     def cacheHit(self):
         '''The current call has found a hit in the cache
@@ -195,6 +195,7 @@
         There's two types of events:
          call events:
           meta_type
+          path
           object
           time_elapsed
           time_start
@@ -237,7 +238,7 @@
         else:
             raise KeyError, name
 
-    def startCall(self, meta_type, object, time_start=None):
+    def startCall(self, meta_type, path, object, time_start=None):
         '''Register a call
         '''
         if time_start is None:
@@ -257,7 +258,7 @@
 
         # now insert the call
         self.current_event = info = {'meta_type': meta_type, 'object': object,
-            'time_start': time_start,
+            'time_start': time_start, 'path': path,
             'time_elapsed': time_start - self.time_start, 'events': []}
         parent['events'].append(info)
         self.stack.append(info)
@@ -537,6 +538,26 @@
         raise KeyError, name
 #
 # $Log$
+# Revision 1.1.2.2  2002/05/20 07:21:37  anthony
+# First commit of merged ZC changes (from Ken, Tres and Shane (according to
+# the rcs dollarLogdollar))
+#
+# Major changes:
+#
+#   You can now wrap/instrument _any_ method, not just the __call__ or _exec
+#   one.
+#
+#   Some changes to the way displays work.
+#
+#   Various cleanups.
+#
+# Note _well_ the second entry in the TODO.txt file: Right now, if the
+# publisher hook is installed, and you refresh this product, you will
+# end up with a trashed Zope that needs a restart. I'm looking at this
+# now. I don't see exactly why this is happening, yet.
+#
+# Still some display cleanup to come, as well.
+#
 # Revision 1.1.2.1  2002/05/17 05:26:53  anthony
 # Initial checkin of CallProfiler branch. This is the internal ekit version.
 #