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