|
1
|
|
|
"""Profiler plugin.""" |
|
2
|
|
|
|
|
3
|
|
|
import sys |
|
4
|
|
|
from collections import Counter |
|
5
|
|
|
|
|
6
|
|
|
from glances.logger import logger |
|
7
|
|
|
from glances.plugins.plugin.model import GlancesPluginModel |
|
8
|
|
|
|
|
9
|
|
|
# Constants for sys.monitoring |
|
10
|
|
|
TOOL_ID = 2 # ID 0 is reserved, 1 was used in test, 2 should be safe |
|
11
|
|
|
# We will use PY_START to count function entries |
|
12
|
|
|
EVENT_ID = getattr(sys.monitoring.events, 'PY_START', None) if hasattr(sys, 'monitoring') else None |
|
13
|
|
|
|
|
14
|
|
|
|
|
15
|
|
|
class PluginModel(GlancesPluginModel): |
|
16
|
|
|
"""Glances' Profiler Plugin. |
|
17
|
|
|
|
|
18
|
|
|
stats is a list of dict (function name, count) |
|
19
|
|
|
""" |
|
20
|
|
|
|
|
21
|
|
|
def __init__(self, args=None, config=None): |
|
22
|
|
|
"""Init the plugin.""" |
|
23
|
|
|
super().__init__(args=args, config=config) |
|
24
|
|
|
|
|
25
|
|
|
# We want to display the stats in the UI |
|
26
|
|
|
self.args = args |
|
27
|
|
|
|
|
28
|
|
|
# Init the internal counter |
|
29
|
|
|
self._counts = Counter() |
|
30
|
|
|
self._monitoring_active = False |
|
31
|
|
|
|
|
32
|
|
|
# Check availability |
|
33
|
|
|
if not hasattr(sys, 'monitoring'): |
|
34
|
|
|
logger.warning("sys.monitoring not available. Profiler plugin disabled.") |
|
35
|
|
|
self.actions.disable() |
|
36
|
|
|
return |
|
37
|
|
|
|
|
38
|
|
|
try: |
|
39
|
|
|
sys.monitoring.use_tool_id(TOOL_ID, "glances_profiler") |
|
40
|
|
|
logger.info(f"sys.monitoring tool ID {TOOL_ID} registered.") |
|
41
|
|
|
self._monitoring_active = True |
|
42
|
|
|
|
|
43
|
|
|
# Register callback |
|
44
|
|
|
sys.monitoring.register_callback(TOOL_ID, EVENT_ID, self._callback) |
|
45
|
|
|
|
|
46
|
|
|
# Enable events |
|
47
|
|
|
sys.monitoring.set_events(TOOL_ID, EVENT_ID) |
|
48
|
|
|
|
|
49
|
|
|
except ValueError as e: |
|
50
|
|
|
logger.error(f"Failed to register sys.monitoring tool: {e}") |
|
51
|
|
|
self.actions.disable() |
|
52
|
|
|
self._monitoring_active = False |
|
53
|
|
|
|
|
54
|
|
|
def exit(self): |
|
55
|
|
|
"""Stop monitoring.""" |
|
56
|
|
|
if self._monitoring_active and hasattr(sys, 'monitoring'): |
|
57
|
|
|
sys.monitoring.set_events(TOOL_ID, 0) |
|
58
|
|
|
sys.monitoring.free_tool_id(TOOL_ID) |
|
59
|
|
|
super().exit() |
|
60
|
|
|
|
|
61
|
|
|
def _callback(self, code, instruction_offset): |
|
62
|
|
|
"""Callback for sys.monitoring.""" |
|
63
|
|
|
# This is called VERY frqeuently. Keep it minimal. |
|
64
|
|
|
# We just increment the counter for the code object name. |
|
65
|
|
|
self._counts[code.co_name] += 1 |
|
66
|
|
|
return sys.monitoring.DISABLE |
|
67
|
|
|
|
|
68
|
|
|
def get_key(self): |
|
69
|
|
|
"""Return the key of the list.""" |
|
70
|
|
|
return 'function' |
|
71
|
|
|
|
|
72
|
|
|
def update_views(self): |
|
73
|
|
|
"""Update the views.""" |
|
74
|
|
|
# Standard table view |
|
75
|
|
|
self.views = {} |
|
76
|
|
|
if not self.stats: |
|
77
|
|
|
return self.views |
|
78
|
|
|
|
|
79
|
|
|
for i in self.stats: |
|
80
|
|
|
self.views[i[self.get_key()]] = {'hidden': False} |
|
81
|
|
|
|
|
82
|
|
|
return self.views |
|
83
|
|
|
|
|
84
|
|
|
def reset(self): |
|
85
|
|
|
"""Reset stats.""" |
|
86
|
|
|
self.stats = [] |
|
87
|
|
|
self._counts.clear() |
|
88
|
|
|
|
|
89
|
|
|
def update(self): |
|
90
|
|
|
"""Update stats.""" |
|
91
|
|
|
# Reset stats |
|
92
|
|
|
self.reset() |
|
93
|
|
|
|
|
94
|
|
|
if not self._monitoring_active: |
|
95
|
|
|
return self.stats |
|
96
|
|
|
|
|
97
|
|
|
# Get the top 10 most frequent functions |
|
98
|
|
|
# We take the counter snapshot and reset it maybe? |
|
99
|
|
|
# Or just show cumulative? Let's show rate (per second/update) if possible. |
|
100
|
|
|
# For now, let's just show top N in the current interval. |
|
101
|
|
|
|
|
102
|
|
|
# NOTE: To show rate, we would need to diff with previous. |
|
103
|
|
|
# But for simplicity V1, let's just show the accumulated counts since start (or allow reset). |
|
104
|
|
|
# Actually, showing "Hot functions right now" implying per-update interval is better. |
|
105
|
|
|
|
|
106
|
|
|
# Snapshot and reset internal counter for the next interval? |
|
107
|
|
|
# WARNING: _callback runs in another thread/context potentially? |
|
108
|
|
|
# In simple Python (GIL), it is safe-ish, but let's be careful. |
|
109
|
|
|
# sys.monitoring callback runs synchronously. |
|
110
|
|
|
|
|
111
|
|
|
# Let's copy the current state |
|
112
|
|
|
current_counts = self._counts.copy() |
|
113
|
|
|
# self._counts.clear() # If we want per-interval stats, we should clear. |
|
114
|
|
|
|
|
115
|
|
|
# Sort by count desc |
|
116
|
|
|
top_n = current_counts.most_common(10) |
|
117
|
|
|
|
|
118
|
|
|
for func_name, count in top_n: |
|
119
|
|
|
stat = {'function': func_name, 'count': count} |
|
120
|
|
|
self.stats.append(stat) |
|
121
|
|
|
|
|
122
|
|
|
return self.stats |
|
123
|
|
|
|