[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.
#