Throw more informative error classes from perf.
[pdm.git] / pdm / perf.py
CommitLineData
5463509c
FT
1"""Python Daemon Management -- PERF utilities
2
3This module serves two purposes: It has a few utility classes
4for implementing PERF interfaces in common ways, and uses those
5classes to implement some standard PERF objects that can be used by
6PERF clients connecting to any PERF server.
7
57808152 8See the documentation for L{pdm.srv.perf} for a description of the
5463509c
FT
9various PERF interfaces.
10
11It contains two named PERF objects:
12
57808152
FT
13 - sysres -- A directory containing the following objects pertaining
14 to the resource usage of the server process:
15
16 - realtime -- An attribute returning the amount of real time since
17 the PDM module was imported (which likely coincides with the
18 amount of time the server process has been running).
19
20 - cputime -- An attribute returning the amount of CPU time
21 consumed by the server process (in both user and kernel mode).
22
23 - utime -- An attribute returning the amount of CPU time the
24 server process has spent in user mode.
25
26 - stime -- An attribute returning the amount of CPU time the
27 server process has spent in kernel mode.
28
29 - maxrss -- An attribute returning the largest resident set size
30 the server process has used during its lifetime.
31
32 - rusage -- An attribute returning the current rusage of the
33 server process.
34
35 - sysinfo -- A directory containing the following objects pertaining
36 to the environment of the server process:
37
38 - pid -- An attribute returning the PID of the server process.
39
40 - uname -- An attribute returning the uname information of the
41 system.
42
43 - hostname -- An attribute returning the hostname of the system.
44
45 - platform -- An attribute returning the Python build platform.
5463509c
FT
46"""
47
796cc8b9 48import os, sys, time, socket, threading
7f97a47e 49
5463509c
FT
50__all__ = ["attrinfo", "simpleattr", "valueattr", "eventobj",
51 "staticdir", "event", "procevent", "startevent",
52 "finishevent"]
53
e7b433ee
FT
54class error(Exception):
55 pass
56class nosuchname(LookupError, error):
57 pass
58class nosuchproto(error):
59 pass
60
7f97a47e 61class attrinfo(object):
5463509c 62 """The return value of the `attrinfo' method on `attr' objects as
57808152 63 described in L{pdm.srv.perf}.
5463509c
FT
64
65 Currently contains a single data field, `desc', which should have
66 a human-readable description of the purpose of the attribute.
67 """
7f97a47e
FT
68 def __init__(self, desc = None):
69 self.desc = desc
70
71class perfobj(object):
72 def __init__(self, *args, **kwargs):
ed115f48 73 super().__init__()
7f97a47e
FT
74
75 def pdm_protocols(self):
76 return []
77
78class simpleattr(perfobj):
5463509c
FT
79 """An implementation of the `attr' interface, which is initialized
80 with a function, and returns whatever that function returns when
81 read.
82 """
7f97a47e 83 def __init__(self, func, info = None, *args, **kwargs):
ed115f48 84 super().__init__(*args, **kwargs)
7f97a47e
FT
85 self.func = func
86 if info is None:
87 info = attrinfo()
88 self.info = info
89
90 def readattr(self):
91 return self.func()
92
93 def attrinfo(self):
94 return self.info
95
96 def pdm_protocols(self):
ed115f48 97 return super().pdm_protocols() + ["attr"]
7f97a47e
FT
98
99class valueattr(perfobj):
5463509c
FT
100 """An implementation of the `attr' interface, which is initialized
101 with a single value, and returns that value when read. Subsequent
102 updates to the value are reflected in subsequent reads.
103 """
7f97a47e 104 def __init__(self, init, info = None, *args, **kwargs):
ed115f48 105 super().__init__(*args, **kwargs)
7f97a47e
FT
106 self.value = init
107 if info is None:
108 info = attrinfo()
109 self.info = info
110
111 def readattr(self):
112 return self.value
113
114 def attrinfo(self):
115 return self.info
116
117 def pdm_protocols(self):
ed115f48 118 return super().pdm_protocols() + ["attr"]
7f97a47e 119
7f97a47e 120class eventobj(perfobj):
5463509c
FT
121 """An implementation of the `event' interface. It keeps track of
122 subscribers, and will multiplex any event to all current
123 subscribers when submitted with the `notify' method.
124 """
7f97a47e 125 def __init__(self, *args, **kwargs):
ed115f48 126 super().__init__(*args, **kwargs)
7f97a47e
FT
127 self.subscribers = set()
128
129 def subscribe(self, cb):
130 if cb in self.subscribers:
131 raise ValueError("Already subscribed")
132 self.subscribers.add(cb)
133
134 def unsubscribe(self, cb):
135 self.subscribers.remove(cb)
136
137 def notify(self, event):
5463509c 138 """Notify all subscribers with the given event object."""
51c86362 139 for cb in list(self.subscribers):
7f97a47e
FT
140 try:
141 cb(event)
142 except: pass
143
144 def pdm_protocols(self):
ed115f48 145 return super().pdm_protocols() + ["event"]
7f97a47e
FT
146
147class staticdir(perfobj):
5463509c
FT
148 """An implementation of the `dir' interface. Put other PERF
149 objects in it using the normal dict assignment syntax, and it will
150 return them to requesting clients.
151 """
7f97a47e 152 def __init__(self, *args, **kwargs):
ed115f48 153 super().__init__(*args, **kwargs)
7f97a47e
FT
154 self.map = {}
155
156 def __setitem__(self, name, ob):
157 self.map[name] = ob
158
159 def __delitem__(self, name):
160 del self.map[name]
161
162 def __getitem__(self, name):
163 return self.map[name]
164
165 def get(self, name, default = None):
166 return self.map.get(name, default)
167
168 def listdir(self):
afd9f04c 169 return list(self.map.keys())
7f97a47e
FT
170
171 def lookup(self, name):
172 return self.map[name]
173
174 def pdm_protocols(self):
ed115f48 175 return super().pdm_protocols() + ["dir"]
7f97a47e 176
0e2552cf
FT
177class simplefunc(perfobj):
178 """An implementation of the `invoke' interface. Put callables in
179 it using the normal dict assignment syntax, and it will call them
180 when invoked with the corresponding method name. Additionally, it
181 updates itself with any keyword-arguments it is initialized with."""
182 def __init__(self, *args, **kwargs):
86b3a0ac 183 super().__init__(*args)
0e2552cf
FT
184 self.map = {}
185 self.map.update(kwargs)
186
187 def __setitem__(self, name, func):
188 self.map[name] = func
189
190 def __delitem__(self, name):
191 del self.map[name]
192
193 def invoke(self, method, *args, **kwargs):
194 if method not in self.map:
195 raise AttributeError(method)
196 self.map[method](*args, **kwargs)
197
198 def pdm_protocols(self):
86b3a0ac 199 return super().pdm_protocols() + ["invoke"]
0e2552cf 200
bcb622d6 201class event(object):
5463509c
FT
202 """This class should be subclassed by all event objects sent via
203 the `event' interface. Its main utility is that it keeps track of
204 the time it was created, so that listening clients can precisely
205 measure the time between event notifications.
206
207 Subclasses should make sure to call the __init__ method if they
208 override it.
209 """
bcb622d6
FT
210 def __init__(self):
211 self.time = time.time()
212
87b07b36
FT
213idlock = threading.Lock()
214procevid = 0
709446f6
FT
215
216def getprocid():
217 global procevid
218 idlock.acquire()
219 try:
220 ret = procevid
221 procevid += 1
222 return ret
223 finally:
224 idlock.release()
225
226class procevent(event):
5463509c
FT
227 """A subclass of the `event' class meant to group several events
228 related to the same process. Create a new process by creating (a
229 subclass of) the `startevent' class, and subsequent events in the
230 same process by passing that startevent as the `id' parameter.
231
232 It works by having `startevent' allocate a unique ID for each
233 process, and every other procevent initializing from that
234 startevent copying the ID. The data field `id' contains the ID so
235 that it can be compared by clients.
236
237 An example of such a process might be a client connection, where a
238 `startevent' is emitted when a client connects, another subclass
239 of `procevent' emitted when the client runs a command, and a
240 `finishevent' emitted when the connection is closed.
241 """
709446f6 242 def __init__(self, id):
ed115f48 243 super().__init__()
709446f6
FT
244 if isinstance(id, procevent):
245 self.id = id.id
246 else:
247 self.id = id
248
249class startevent(procevent):
5463509c 250 """A subclass of `procevent'. See its documentation for details."""
87b07b36 251 def __init__(self):
ed115f48 252 super().__init__(getprocid())
709446f6
FT
253
254class finishevent(procevent):
5463509c
FT
255 """A subclass of `procevent'. Intended to be emitted when a
256 process finishes and terminates. The `aborted' field can be used
257 to indicate whether the process completed successfully, if such a
258 distinction is meaningful. The `start' parameter should be the
259 `startevent' instance used when the process was initiated."""
0f1f2a1b 260 def __init__(self, start, aborted = False):
ed115f48 261 super().__init__(start)
87b07b36
FT
262 self.aborted = aborted
263
7f97a47e
FT
264sysres = staticdir()
265itime = time.time()
7f97a47e 266sysres["realtime"] = simpleattr(func = lambda: time.time() - itime)
796cc8b9
FT
267try:
268 import resource
269except ImportError:
270 pass
271else:
272 ires = resource.getrusage(resource.RUSAGE_SELF)
273 def ct():
274 ru = resource.getrusage(resource.RUSAGE_SELF)
275 return (ru.ru_utime - ires.ru_utime) + (ru.ru_stime - ires.ru_stime)
276 sysres["cputime"] = simpleattr(func = ct)
277 sysres["utime"] = simpleattr(func = lambda: resource.getrusage(resource.RUSAGE_SELF).ru_utime - ires.ru_utime)
278 sysres["stime"] = simpleattr(func = lambda: resource.getrusage(resource.RUSAGE_SELF).ru_stime - ires.ru_stime)
279 sysres["maxrss"] = simpleattr(func = lambda: resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)
280 sysres["rusage"] = simpleattr(func = lambda: resource.getrusage(resource.RUSAGE_SELF))
7f97a47e
FT
281
282sysinfo = staticdir()
283sysinfo["pid"] = simpleattr(func = os.getpid)
284sysinfo["uname"] = simpleattr(func = os.uname)
285sysinfo["hostname"] = simpleattr(func = socket.gethostname)
286sysinfo["platform"] = valueattr(init = sys.platform)
0e2552cf 287
6e858b0f
FT
288def reload(modname):
289 mod = sys.modules.get(modname)
290 if mod is None:
291 raise ValueError(modname)
292 import importlib
293 importlib.reload(mod)
294
295sysctl = simplefunc(exit=lambda status=0: os._exit(status),
296 reload=reload)