Skip to content

Commit

Permalink
Restore the ZMI Debug Information control panel page (#899)
Browse files Browse the repository at this point in the history
* - Restore the ZMI `Debug Information` control panel page

* - use bug icon

* include `class_types` in refcount analysis

Co-authored-by: dieter <dieter@handshake.de>
  • Loading branch information
dataflake and d-maurer committed Oct 8, 2020
1 parent 48961ab commit 531706d
Show file tree
Hide file tree
Showing 9 changed files with 510 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ https://github.com/zopefoundation/Zope/blob/4.x/CHANGES.rst
5.1 (unreleased)
----------------

- Restore the ZMI `Debug Information` control panel page
(`#898 <https://github.com/zopefoundation/Zope/issues/898>`_)

- Update to newest versions of dependencies.


Expand Down
114 changes: 113 additions & 1 deletion src/App/ApplicationManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,21 @@
import os
import sys
import time
import types
from _thread import get_ident
from urllib import parse

from AccessControl.class_init import InitializeClass
from AccessControl.requestmethod import requestmethod
from Acquisition import Implicit
from App.CacheManager import CacheManager
from App.config import getConfiguration
from App.DavLockManager import DavLockManager
from App.Management import Tabs
from App.special_dtml import DTMLFile
from App.Undo import UndoSupport
from App.version_txt import version_txt
from DateTime.DateTime import DateTime
from OFS.Traversable import Traversable
from Persistence import Persistent
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
Expand Down Expand Up @@ -58,6 +62,8 @@ class DatabaseChooser(Tabs, Traversable, Implicit):
{'label': 'Control Panel', 'action': '../manage_main'},
{'label': 'Databases', 'action': 'manage_main'},
{'label': 'Configuration', 'action': '../Configuration/manage_main'},
{'label': 'DAV Locks', 'action': '../DAVLocks/manage_main'},
{'label': 'Debug Information', 'action': '../DebugInfo/manage_main'},
)
MANAGE_TABS_NO_BANNER = True

Expand Down Expand Up @@ -102,6 +108,7 @@ class ConfigurationViewer(Tabs, Traversable, Implicit):
{'label': 'Databases', 'action': '../Database/manage_main'},
{'label': 'Configuration', 'action': 'manage_main'},
{'label': 'DAV Locks', 'action': '../DavLocks/manage_main'},
{'label': 'Debug Information', 'action': '../DebugInfo/manage_main'},
)
MANAGE_TABS_NO_BANNER = True

Expand Down Expand Up @@ -132,7 +139,107 @@ def manage_getConfiguration(self):
InitializeClass(ConfigurationViewer)


class ApplicationManager(Persistent, Tabs, Traversable, Implicit):
# refcount snapshot info
_v_rcs = None
_v_rst = None


class DebugManager(Tabs, Traversable, Implicit):
""" Debug and profiling information
"""
manage = manage_main = manage_workspace = DTMLFile('dtml/debug', globals())
manage_main._setName('manage_main')
id = 'DebugInfo'
name = title = 'Debug Information'
meta_type = name
zmi_icon = 'fas fa-bug'

manage_options = (
{'label': 'Control Panel', 'action': '../manage_main'},
{'label': 'Databases', 'action': '../Database/manage_main'},
{'label': 'Configuration', 'action': '../Configuration/manage_main'},
{'label': 'DAV Locks', 'action': '../DAVLocks/manage_main'},
{'label': 'Debug Information', 'action': 'manage_main'},
)

def refcount(self, n=None, t=(type(Implicit), type(object))):
# return class reference info
counts = {}
for m in list(sys.modules.values()):
if m is None:
continue
if not isinstance(m, types.ModuleType) or 'six.' in m.__name__:
continue
for sym in dir(m):
ob = getattr(m, sym)
if type(ob) in t:
counts[ob] = sys.getrefcount(ob)
pairs = []
for ob, v in counts.items():
ob_name = getattr(ob, "__name__", "unknown")
if hasattr(ob, '__module__'):
name = f'{ob.__module__}.{ob_name}'
else:
name = '%s' % ob_name
pairs.append((v, name))
pairs.sort()
pairs.reverse()
if n is not None:
pairs = pairs[:n]
return pairs

def refdict(self):
counts = {}
for v, n in self.refcount():
counts[n] = v
return counts

def rcsnapshot(self):
global _v_rcs
global _v_rst
_v_rcs = self.refdict()
_v_rst = DateTime()

def rcdate(self):
return _v_rst

def rcdeltas(self):
if _v_rcs is None:
self.rcsnapshot()
nc = self.refdict()
rc = _v_rcs
rd = []
for n, c in nc.items():
try:
prev = rc.get(n, 0)
if c > prev:
rd.append((c - prev, (c, prev, n)))
except Exception:
pass
rd.sort()
rd.reverse()
return [{'name': n[1][2],
'delta': n[0],
'pc': n[1][1],
'rc': n[1][0],
} for n in rd]

def dbconnections(self):
import Zope2 # for data
return Zope2.DB.connectionDebugInfo()

def manage_getSysPath(self):
return list(sys.path)


InitializeClass(DebugManager)


class ApplicationManager(CacheManager,
Persistent,
Tabs,
Traversable,
Implicit):
"""System management
"""
__allow_access_to_unprotected_subobjects__ = 1
Expand All @@ -147,6 +254,7 @@ class ApplicationManager(Persistent, Tabs, Traversable, Implicit):
Database = DatabaseChooser()
Configuration = ConfigurationViewer()
DavLocks = DavLockManager()
DebugInfo = DebugManager()

manage = manage_main = DTMLFile('dtml/cpContents', globals())
manage_main._setName('manage_main')
Expand All @@ -155,6 +263,7 @@ class ApplicationManager(Persistent, Tabs, Traversable, Implicit):
{'label': 'Databases', 'action': 'Database/manage_main'},
{'label': 'Configuration', 'action': 'Configuration/manage_main'},
{'label': 'DAV Locks', 'action': 'DavLocks/manage_main'},
{'label': 'Debug Information', 'action': 'DebugInfo/manage_main'},
)
MANAGE_TABS_NO_BANNER = True

Expand Down Expand Up @@ -189,6 +298,9 @@ def sys_version(self):
def sys_platform(self):
return sys.platform

def thread_get_ident(self):
return get_ident()

def debug_mode(self):
return getConfiguration().debug_mode

Expand Down
86 changes: 86 additions & 0 deletions src/App/CacheManager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Cache management support.
This class is mixed into the application manager in App.ApplicationManager.
"""

from AccessControl.class_init import InitializeClass
from Acquisition import aq_parent


class CacheManager:
"""Cache management mix-in
"""

def _getDB(self):
try:
return self._p_jar.db()
except AttributeError:
return aq_parent(self)._p_jar.db()

def cache_size(self):
db = self._getDB()
return db.getCacheSize()

def cache_detail(self, REQUEST=None):
"""
Returns the name of the classes of the objects in the cache
and the number of objects in the cache for each class.
"""
detail = self._getDB().cacheDetail()
if REQUEST is not None:
# format as text
REQUEST.RESPONSE.setHeader('Content-Type', 'text/plain')
return '\n'.join(
['%6d %s' % (count, name) for name, count in detail])
# raw
return detail

def cache_extreme_detail(self, REQUEST=None):
"""
Returns information about each object in the cache.
"""
detail = self._getDB().cacheExtremeDetail()
if REQUEST is not None:
# sort the list.
lst = [((dict['conn_no'], dict['oid']), dict) for dict in detail]
# format as text.
res = [
'# Table shows connection number, oid, refcount, state, '
'and class.',
'# States: L = loaded, G = ghost, C = changed']
for sortkey, dict in lst:
id = dict.get('id', None)
if id:
idinfo = ' (%s)' % id
else:
idinfo = ''
s = dict['state']
if s == 0:
state = 'L' # loaded
elif s == 1:
state = 'C' # changed
else:
state = 'G' # ghost
res.append('%d %-34s %6d %s %s%s' % (
dict['conn_no'], repr(dict['oid']), dict['rc'],
state, dict['klass'], idinfo))
REQUEST.RESPONSE.setHeader('Content-Type', 'text/plain')
return '\n'.join(res)
else:
# raw
return detail


InitializeClass(CacheManager)
10 changes: 8 additions & 2 deletions src/App/DavLockManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ class DavLockManager(Item, Implicit):

security.declareProtected(webdav_manage_locks, # NOQA: D001
'manage_davlocks')
manage_davlocks = manage_main = manage = DTMLFile(
manage_davlocks = manage_main = manage = manage_workspace = DTMLFile(
'dtml/davLockManager', globals())
manage_davlocks._setName('manage_davlocks')
manage_options = ({'label': 'Write Locks', 'action': 'manage_main'}, )
manage_options = (
{'label': 'Control Panel', 'action': '../manage_main'},
{'label': 'Databases', 'action': 'manage_main'},
{'label': 'Configuration', 'action': '../Configuration/manage_main'},
{'label': 'DAV Locks', 'action': 'manage_main'},
{'label': 'Debug Information', 'action': '../DebugInfo/manage_main'},
)

@security.protected(webdav_manage_locks)
def findLockedObjects(self, frompath=''):
Expand Down
4 changes: 3 additions & 1 deletion src/App/dtml/davLockManager.dtml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<dtml-var manage_page_header>
<dtml-var manage_tabs>
<dtml-with "_(management_view='DAV Locks')">
<dtml-var manage_tabs>
</dtml-with>

<main class="container-fluid">

Expand Down
Loading

0 comments on commit 531706d

Please sign in to comment.