MATE-runtime 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- MATE/__init__.py +22 -0
- MATE/__main__.py +7 -0
- MATE/cli/__init__.py +181 -0
- MATE/cli/commands.py +573 -0
- MATE/cli/workdir.py +571 -0
- MATE/compat/__init__.py +1 -0
- MATE/compat/interface.py +248 -0
- MATE/compat/sizes.py +144 -0
- MATE/config/__init__.py +1 -0
- MATE/config/axes.py +310 -0
- MATE/config/builders.py +498 -0
- MATE/config/calendar.py +371 -0
- MATE/config/canonical.py +543 -0
- MATE/config/criteria.py +396 -0
- MATE/config/events.py +183 -0
- MATE/config/invariance.py +227 -0
- MATE/config/loader.py +498 -0
- MATE/config/model.py +883 -0
- MATE/config/resolve.py +943 -0
- MATE/config/runtable.py +342 -0
- MATE/config/seeds.py +344 -0
- MATE/config/stages.py +632 -0
- MATE/config/structure.py +635 -0
- MATE/config/toml.py +237 -0
- MATE/conformance/__init__.py +14 -0
- MATE/conformance/baseline.py +250 -0
- MATE/conformance/checks.py +1604 -0
- MATE/conformance/sampler.py +179 -0
- MATE/errors.py +236 -0
- MATE/evaluation/__init__.py +1 -0
- MATE/evaluation/budget.py +160 -0
- MATE/evaluation/invalids.py +181 -0
- MATE/evaluation/requests.py +148 -0
- MATE/execution/__init__.py +1 -0
- MATE/execution/accelerator.py +60 -0
- MATE/execution/base.py +643 -0
- MATE/execution/choose.py +152 -0
- MATE/execution/devices.py +108 -0
- MATE/execution/process.py +377 -0
- MATE/execution/serial.py +37 -0
- MATE/execution/thread.py +106 -0
- MATE/execution/vectorised.py +32 -0
- MATE/launch.py +267 -0
- MATE/loop/__init__.py +6 -0
- MATE/loop/continuous.py +172 -0
- MATE/loop/driver.py +783 -0
- MATE/loop/events.py +266 -0
- MATE/loop/generational.py +140 -0
- MATE/loop/owns_loop.py +333 -0
- MATE/loop/subpops.py +508 -0
- MATE/modules/__init__.py +1 -0
- MATE/modules/construct.py +198 -0
- MATE/modules/discovery.py +238 -0
- MATE/modules/registry.py +970 -0
- MATE/modules/software.py +593 -0
- MATE/modules/versions.py +72 -0
- MATE/numerics.py +159 -0
- MATE/observation/__init__.py +10 -0
- MATE/observation/control.py +199 -0
- MATE/observation/hub.py +451 -0
- MATE/observation/records.py +125 -0
- MATE/observation/sinks.py +313 -0
- MATE/provenance.py +396 -0
- MATE/readout/__init__.py +10 -0
- MATE/readout/catalogue.py +282 -0
- MATE/readout/front.py +151 -0
- MATE/readout/readout.py +927 -0
- MATE/readout/series.py +155 -0
- MATE/readout/statistics.py +202 -0
- MATE/run/__init__.py +1 -0
- MATE/run/checkpointing.py +266 -0
- MATE/run/contexts.py +553 -0
- MATE/run/declarations.py +240 -0
- MATE/run/plan.py +464 -0
- MATE/run/preconditions.py +165 -0
- MATE/run/run.py +991 -0
- MATE/run/specs.py +319 -0
- MATE/run/times.py +103 -0
- MATE/scheduling/__init__.py +214 -0
- MATE/scheduling/local.py +119 -0
- MATE/scheduling/slurm.py +380 -0
- MATE/storage/__init__.py +1 -0
- MATE/storage/checkpoint.py +414 -0
- MATE/storage/codec.py +315 -0
- MATE/storage/database.py +359 -0
- MATE/storage/lease.py +397 -0
- MATE/storage/rundir.py +248 -0
- mate_runtime-1.0.0.dist-info/METADATA +58 -0
- mate_runtime-1.0.0.dist-info/RECORD +93 -0
- mate_runtime-1.0.0.dist-info/WHEEL +4 -0
- mate_runtime-1.0.0.dist-info/entry_points.txt +2 -0
- mate_runtime-1.0.0.dist-info/licenses/COPYING +674 -0
- mate_runtime-1.0.0.dist-info/licenses/COPYING.LESSER +165 -0
MATE/observation/hub.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""The observer of a run: loggers, emitters, worker channels and the dispatch to sinks.
|
|
2
|
+
|
|
3
|
+
Threading rule (design §3.6): sinks and ``on_emit`` are called only from the run's driver
|
|
4
|
+
thread. Process and thread workers log and emit through a :class:`WorkerChannel`; the driver
|
|
5
|
+
moves their records to the sinks with :meth:`Observer.drain`. An emitter may hold its
|
|
6
|
+
emissions instead: the executor hands them over with :meth:`Observer.receive` once the
|
|
7
|
+
results of the work that made them have returned.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import multiprocessing
|
|
11
|
+
import queue
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
15
|
+
from dataclasses import dataclass, replace
|
|
16
|
+
from typing import Any, Literal
|
|
17
|
+
|
|
18
|
+
from MATE_contract import EventSink, Logger
|
|
19
|
+
|
|
20
|
+
from MATE.observation.records import LEVELS, LOGGER_LEVELS, Emission, EventRecord, LogRecord
|
|
21
|
+
from MATE.observation.sinks import Sink
|
|
22
|
+
|
|
23
|
+
__all__ = ["Observer", "WorkerChannel"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DEBUG, _INFO, _WARNING, _ERROR = (LOGGER_LEVELS[name]
|
|
27
|
+
for name in ("debug", "info", "warning", "error"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _threshold(verbosity: str) -> int:
|
|
31
|
+
"""Return the threshold of a verbosity name."""
|
|
32
|
+
try:
|
|
33
|
+
return LEVELS[verbosity]
|
|
34
|
+
except KeyError:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"verbosity {verbosity!r}: unknown; expected one of {', '.join(LEVELS)}") from None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _format(msg: str, args: tuple[object, ...]) -> str:
|
|
40
|
+
"""Return ``msg % args``, or the raw pieces when they do not match."""
|
|
41
|
+
if not args:
|
|
42
|
+
return msg
|
|
43
|
+
try:
|
|
44
|
+
return msg % args
|
|
45
|
+
except (TypeError, ValueError, KeyError):
|
|
46
|
+
return f"{msg} {args!r}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _Logger:
|
|
50
|
+
"""A logger whose level test precedes any formatting ([K] LG1)."""
|
|
51
|
+
|
|
52
|
+
__slots__ = ("_path", "_level_of", "_write")
|
|
53
|
+
|
|
54
|
+
def __init__(self, path: str, level_of: Callable[[], int],
|
|
55
|
+
write: Callable[[int, str, str], None]) -> None:
|
|
56
|
+
self._path = path
|
|
57
|
+
self._level_of = level_of
|
|
58
|
+
self._write = write
|
|
59
|
+
|
|
60
|
+
def _log(self, level: int, msg: str, args: tuple[object, ...]) -> None:
|
|
61
|
+
"""Format and write one message if its level passes the threshold."""
|
|
62
|
+
if level < self._level_of():
|
|
63
|
+
return
|
|
64
|
+
self._write(level, self._path, _format(msg, args))
|
|
65
|
+
|
|
66
|
+
def debug(self, msg: str, /, *args: object) -> None:
|
|
67
|
+
"""Log a debugging message (level 10)."""
|
|
68
|
+
self._log(_DEBUG, msg, args)
|
|
69
|
+
|
|
70
|
+
def info(self, msg: str, /, *args: object) -> None:
|
|
71
|
+
"""Log an informative message (level 30)."""
|
|
72
|
+
self._log(_INFO, msg, args)
|
|
73
|
+
|
|
74
|
+
def warning(self, msg: str, /, *args: object) -> None:
|
|
75
|
+
"""Log a warning (level 35)."""
|
|
76
|
+
self._log(_WARNING, msg, args)
|
|
77
|
+
|
|
78
|
+
def error(self, msg: str, /, *args: object) -> None:
|
|
79
|
+
"""Log an error (level 40)."""
|
|
80
|
+
self._log(_ERROR, msg, args)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _Emitter:
|
|
84
|
+
"""An event sink bound to one component; builds an :class:`Emission` per call."""
|
|
85
|
+
|
|
86
|
+
__slots__ = ("_bound", "_clock", "_send")
|
|
87
|
+
|
|
88
|
+
def __init__(self, bound: Mapping[str, Any], clock: Callable[[], tuple[int | None, object]],
|
|
89
|
+
send: Callable[[Emission], None]) -> None:
|
|
90
|
+
self._bound = bound
|
|
91
|
+
self._clock = clock
|
|
92
|
+
self._send = send
|
|
93
|
+
|
|
94
|
+
def __call__(self, name: str, /, **fields: object) -> None:
|
|
95
|
+
"""Emit the event ``name`` with its fields."""
|
|
96
|
+
b = self._bound
|
|
97
|
+
generation, date = b["run_generation"], b["date"]
|
|
98
|
+
if generation is None or date is None:
|
|
99
|
+
now_generation, now_date = self._clock()
|
|
100
|
+
generation = now_generation if generation is None else generation
|
|
101
|
+
date = now_date if date is None else date
|
|
102
|
+
self._send(Emission(origin=b["origin"], module=b["module"], subpop=b["subpop"],
|
|
103
|
+
path=b["path"], name=name, fields=fields, run_generation=generation,
|
|
104
|
+
date=date, request_ranks=b["request_ranks"], wall=time.time()))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _no_clock() -> tuple[None, None]:
|
|
108
|
+
"""The clock of a worker: generation and date are given at emitter creation."""
|
|
109
|
+
return None, None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class WorkerChannel:
|
|
114
|
+
"""The handle a worker logs and emits through ([X] SK2).
|
|
115
|
+
|
|
116
|
+
A channel of kind ``"process"`` holds a multiprocessing queue and event: it is handed to a
|
|
117
|
+
worker process at its creation (process or pool initializer arguments). A channel of kind
|
|
118
|
+
``"thread"`` holds a ``queue.SimpleQueue`` and a ``threading.Event``.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
kind : {"process", "thread"}
|
|
123
|
+
records : queue
|
|
124
|
+
Where the worker puts its records, drained by :meth:`Observer.drain`.
|
|
125
|
+
stop : event
|
|
126
|
+
The stop flag ([K] CT7, [X] GU5).
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
kind: Literal["process", "thread"]
|
|
130
|
+
records: Any
|
|
131
|
+
stop: Any
|
|
132
|
+
|
|
133
|
+
def logger(self, path: str, verbosity: str) -> Logger:
|
|
134
|
+
"""Return a logger for a component of the worker.
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
path : str
|
|
139
|
+
Log path of the component.
|
|
140
|
+
verbosity : str
|
|
141
|
+
Threshold applied in the worker, the run's initial verbosity (DD-13).
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
Logger
|
|
146
|
+
Messages below the threshold are neither formatted nor sent.
|
|
147
|
+
"""
|
|
148
|
+
threshold = _threshold(verbosity)
|
|
149
|
+
put = self.records.put
|
|
150
|
+
return _Logger(path, lambda: threshold,
|
|
151
|
+
lambda level, p, text: put(("m", level, p, text, time.time())))
|
|
152
|
+
|
|
153
|
+
def emitter(self, *, origin: str, module: str, subpop: str | None, path: str,
|
|
154
|
+
request_ranks: tuple[int, ...] | None, run_generation: int | None,
|
|
155
|
+
date: object, hold: list[Emission] | None = None) -> EventSink:
|
|
156
|
+
"""Return an event sink for a component of the worker.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
origin : {"system", "method"}
|
|
161
|
+
module : str
|
|
162
|
+
subpop : str or None
|
|
163
|
+
path : str
|
|
164
|
+
request_ranks : tuple of int or None
|
|
165
|
+
Candidate index -> request rank, for an evaluate batch.
|
|
166
|
+
run_generation : int or None
|
|
167
|
+
date : object
|
|
168
|
+
hold : list of Emission, optional
|
|
169
|
+
Where the emissions are kept instead of being sent; the executor returns them
|
|
170
|
+
with the results of the work that made them.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
EventSink
|
|
175
|
+
Each call sends one :class:`Emission` to the driver, or appends it to ``hold``.
|
|
176
|
+
"""
|
|
177
|
+
bound = {"origin": origin, "module": module, "subpop": subpop, "path": path,
|
|
178
|
+
"request_ranks": request_ranks, "run_generation": run_generation, "date": date}
|
|
179
|
+
if hold is not None:
|
|
180
|
+
return _Emitter(bound, _no_clock, hold.append)
|
|
181
|
+
put = self.records.put
|
|
182
|
+
return _Emitter(bound, _no_clock, lambda emission: put(("e", emission)))
|
|
183
|
+
|
|
184
|
+
def stopping(self) -> bool:
|
|
185
|
+
"""Whether the run is stopping ([K] CT7, [X] GU5).
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
bool
|
|
190
|
+
"""
|
|
191
|
+
return self.stop.is_set()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Observer:
|
|
195
|
+
"""The observation hub of one run ([X] §17).
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
run : str
|
|
200
|
+
The run directory name, written in every record.
|
|
201
|
+
sinks : sequence of Sink
|
|
202
|
+
Where records go, in order.
|
|
203
|
+
verbosity : {"debug", "verbose", "normal", "quiet"}
|
|
204
|
+
The message threshold ([X] OB3).
|
|
205
|
+
clock : callable, optional
|
|
206
|
+
Returns the current ``(run_generation, date)``, used for emissions and events that
|
|
207
|
+
do not carry them.
|
|
208
|
+
|
|
209
|
+
Attributes
|
|
210
|
+
----------
|
|
211
|
+
on_emit : callable or None
|
|
212
|
+
Receives every module emission; set by the run to the readout. When ``None``,
|
|
213
|
+
emissions go to the event stream as they are.
|
|
214
|
+
|
|
215
|
+
Notes
|
|
216
|
+
-----
|
|
217
|
+
Implements [X] OB2, SK1, SK2 and [K] LG1. Records are numbered by one counter shared by
|
|
218
|
+
messages and events. Sinks and ``on_emit`` are called from the driver thread only.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, run: str, sinks: Sequence[Sink], verbosity: str, *,
|
|
222
|
+
clock: Callable[[], tuple[int | None, object]] = lambda: (None, None)) -> None:
|
|
223
|
+
self.run = run
|
|
224
|
+
self._sinks = list(sinks)
|
|
225
|
+
self._threshold = _threshold(verbosity)
|
|
226
|
+
self._clock = clock
|
|
227
|
+
self._seq = 0
|
|
228
|
+
self._channels: dict[str, WorkerChannel] = {}
|
|
229
|
+
self._stopping = False
|
|
230
|
+
self._closed = False
|
|
231
|
+
self.on_emit: Callable[[Emission], None] | None = None
|
|
232
|
+
|
|
233
|
+
def _next(self) -> int:
|
|
234
|
+
"""Return the next record rank."""
|
|
235
|
+
self._seq += 1
|
|
236
|
+
return self._seq
|
|
237
|
+
|
|
238
|
+
def _write(self, level: int, path: str, text: str, wall: float | None = None) -> None:
|
|
239
|
+
"""Send one formatted message to the sinks."""
|
|
240
|
+
record = LogRecord(self.run, self._next(), time.time() if wall is None else wall,
|
|
241
|
+
level, path, text)
|
|
242
|
+
for sink in self._sinks:
|
|
243
|
+
sink.message(record)
|
|
244
|
+
|
|
245
|
+
def receive(self, emission: Emission) -> None:
|
|
246
|
+
"""Hand a module emission to ``on_emit``, or straight to the event stream.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
emission : Emission
|
|
251
|
+
|
|
252
|
+
Notes
|
|
253
|
+
-----
|
|
254
|
+
Called from the driver thread: by its emitters, by :meth:`drain`, and by an executor
|
|
255
|
+
releasing the emissions it held for work whose results have returned.
|
|
256
|
+
"""
|
|
257
|
+
if self.on_emit is not None:
|
|
258
|
+
self.on_emit(emission)
|
|
259
|
+
else:
|
|
260
|
+
self.publish(EventRecord(self.run, 0, emission.wall, emission.path, emission.name,
|
|
261
|
+
emission.run_generation, emission.date,
|
|
262
|
+
dict(emission.fields)))
|
|
263
|
+
|
|
264
|
+
def logger(self, path: str) -> Logger:
|
|
265
|
+
"""Return the logger of a component of the driver process.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
path : str
|
|
270
|
+
Log path of the component ([X] OB3).
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
Logger
|
|
275
|
+
The level test comes before any formatting ([K] LG1); the threshold followed is
|
|
276
|
+
the observer's current one.
|
|
277
|
+
"""
|
|
278
|
+
return _Logger(path, lambda: self._threshold, self._write)
|
|
279
|
+
|
|
280
|
+
def emitter(self, *, origin: str, module: str, subpop: str | None, path: str,
|
|
281
|
+
request_ranks: tuple[int, ...] | None = None,
|
|
282
|
+
run_generation: int | None = None, date: object = None,
|
|
283
|
+
hold: list[Emission] | None = None) -> EventSink:
|
|
284
|
+
"""Return the event sink of a component of the driver process.
|
|
285
|
+
|
|
286
|
+
Parameters
|
|
287
|
+
----------
|
|
288
|
+
origin : {"system", "method"}
|
|
289
|
+
module : str
|
|
290
|
+
subpop : str or None
|
|
291
|
+
path : str
|
|
292
|
+
request_ranks : tuple of int or None, default None
|
|
293
|
+
run_generation : int or None, default None
|
|
294
|
+
``None``: read from the clock at each emission.
|
|
295
|
+
date : object, default None
|
|
296
|
+
``None``: read from the clock at each emission.
|
|
297
|
+
hold : list of Emission, optional
|
|
298
|
+
Where the emissions are kept instead of being received at once; the executor
|
|
299
|
+
hands them over with :meth:`receive` when the results of their work return.
|
|
300
|
+
|
|
301
|
+
Returns
|
|
302
|
+
-------
|
|
303
|
+
EventSink
|
|
304
|
+
Each call builds one :class:`Emission` and hands it to ``on_emit``, or appends it
|
|
305
|
+
to ``hold``.
|
|
306
|
+
"""
|
|
307
|
+
bound = {"origin": origin, "module": module, "subpop": subpop, "path": path,
|
|
308
|
+
"request_ranks": request_ranks, "run_generation": run_generation, "date": date}
|
|
309
|
+
return _Emitter(bound, self._clock, self.receive if hold is None else hold.append)
|
|
310
|
+
|
|
311
|
+
def message(self, level: int, path: str, text: str) -> None:
|
|
312
|
+
"""Write one of the runtime's own messages.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
level : int
|
|
317
|
+
10 to 40; 20 for the runtime's detail.
|
|
318
|
+
path : str
|
|
319
|
+
text : str
|
|
320
|
+
"""
|
|
321
|
+
if level >= self._threshold:
|
|
322
|
+
self._write(level, path, text)
|
|
323
|
+
|
|
324
|
+
def trace(self, path: str, text: str) -> None:
|
|
325
|
+
"""Hand a traceback to every sink at the debug level, whatever the verbosity.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
path : str
|
|
330
|
+
text : str
|
|
331
|
+
The traceback.
|
|
332
|
+
|
|
333
|
+
Notes
|
|
334
|
+
-----
|
|
335
|
+
[X] RN5, S4: a traceback goes to the debug log. Each sink applies its own threshold,
|
|
336
|
+
so the file sink writes it to ``debug.log`` only and the terminal skips it.
|
|
337
|
+
"""
|
|
338
|
+
self._write(10, path, text)
|
|
339
|
+
|
|
340
|
+
def event(self, name: str, *, path: str = "run", **fields: object) -> None:
|
|
341
|
+
"""Publish one of the runtime's own events (design §3.15).
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
name : str
|
|
346
|
+
``run.start``, ``generation.end``, ``checkpoint``, ...
|
|
347
|
+
path : str, default "run"
|
|
348
|
+
**fields : object
|
|
349
|
+
Plain data.
|
|
350
|
+
"""
|
|
351
|
+
generation, date = self._clock()
|
|
352
|
+
self.publish(EventRecord(self.run, 0, time.time(), path, name, generation, date,
|
|
353
|
+
dict(fields)))
|
|
354
|
+
|
|
355
|
+
def publish(self, record: EventRecord) -> None:
|
|
356
|
+
"""Send an event to the sinks.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
record : EventRecord
|
|
361
|
+
Its ``run`` and ``seq`` are set by the observer.
|
|
362
|
+
"""
|
|
363
|
+
record = replace(record, run=self.run, seq=self._next())
|
|
364
|
+
for sink in self._sinks:
|
|
365
|
+
sink.event(record)
|
|
366
|
+
|
|
367
|
+
def set_verbosity(self, verbosity: str) -> None:
|
|
368
|
+
"""Change the threshold of the observer and of every sink that has one ([X] GU5).
|
|
369
|
+
|
|
370
|
+
Parameters
|
|
371
|
+
----------
|
|
372
|
+
verbosity : {"debug", "verbose", "normal", "quiet"}
|
|
373
|
+
|
|
374
|
+
Raises
|
|
375
|
+
------
|
|
376
|
+
ValueError
|
|
377
|
+
For an unknown verbosity.
|
|
378
|
+
"""
|
|
379
|
+
self._threshold = _threshold(verbosity)
|
|
380
|
+
for sink in self._sinks:
|
|
381
|
+
setter = getattr(sink, "set_verbosity", None)
|
|
382
|
+
if setter is not None:
|
|
383
|
+
setter(verbosity)
|
|
384
|
+
|
|
385
|
+
def channel(self, kind: Literal["process", "thread"]) -> WorkerChannel:
|
|
386
|
+
"""Return the worker channel of a kind, created on first call.
|
|
387
|
+
|
|
388
|
+
Parameters
|
|
389
|
+
----------
|
|
390
|
+
kind : {"process", "thread"}
|
|
391
|
+
|
|
392
|
+
Returns
|
|
393
|
+
-------
|
|
394
|
+
WorkerChannel
|
|
395
|
+
``"process"``: a queue and an event of the ``forkserver`` context;
|
|
396
|
+
``"thread"``: a ``queue.SimpleQueue`` and a ``threading.Event``.
|
|
397
|
+
"""
|
|
398
|
+
channel = self._channels.get(kind)
|
|
399
|
+
if channel is None:
|
|
400
|
+
if kind == "process":
|
|
401
|
+
context = multiprocessing.get_context("forkserver")
|
|
402
|
+
channel = WorkerChannel("process", context.Queue(), context.Event())
|
|
403
|
+
elif kind == "thread":
|
|
404
|
+
channel = WorkerChannel("thread", queue.SimpleQueue(), threading.Event())
|
|
405
|
+
else:
|
|
406
|
+
raise ValueError(f"channel kind {kind!r}: unknown; expected 'process' or 'thread'")
|
|
407
|
+
if self._stopping:
|
|
408
|
+
channel.stop.set()
|
|
409
|
+
self._channels[kind] = channel
|
|
410
|
+
return channel
|
|
411
|
+
|
|
412
|
+
def drain(self) -> None:
|
|
413
|
+
"""Move the records the workers have sent to the sinks and ``on_emit``.
|
|
414
|
+
|
|
415
|
+
Notes
|
|
416
|
+
-----
|
|
417
|
+
Called by the driver thread only; records of one channel keep their arrival order.
|
|
418
|
+
Worker messages are filtered again at the current threshold.
|
|
419
|
+
"""
|
|
420
|
+
for channel in self._channels.values():
|
|
421
|
+
while True:
|
|
422
|
+
try:
|
|
423
|
+
item = channel.records.get_nowait()
|
|
424
|
+
except queue.Empty:
|
|
425
|
+
break
|
|
426
|
+
if item[0] == "m":
|
|
427
|
+
_, level, path, text, wall = item
|
|
428
|
+
if level >= self._threshold:
|
|
429
|
+
self._write(level, path, text, wall)
|
|
430
|
+
else:
|
|
431
|
+
self.receive(item[1])
|
|
432
|
+
|
|
433
|
+
def set_stopping(self) -> None:
|
|
434
|
+
"""Set the stop flag of every channel, present and future ([K] CT7)."""
|
|
435
|
+
self._stopping = True
|
|
436
|
+
for channel in self._channels.values():
|
|
437
|
+
channel.stop.set()
|
|
438
|
+
|
|
439
|
+
def close(self) -> None:
|
|
440
|
+
"""Drain the channels, close the sinks and release the queues."""
|
|
441
|
+
if self._closed:
|
|
442
|
+
return
|
|
443
|
+
self.drain()
|
|
444
|
+
self._closed = True
|
|
445
|
+
for sink in self._sinks:
|
|
446
|
+
sink.close()
|
|
447
|
+
process = self._channels.get("process")
|
|
448
|
+
if process is not None:
|
|
449
|
+
process.records.close()
|
|
450
|
+
process.records.join_thread()
|
|
451
|
+
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Records of the two observation streams: messages for humans and structured events.
|
|
2
|
+
|
|
3
|
+
Levels are integers ([X] OB3, DD-13): a verbosity names a threshold, a logger method a level.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
__all__ = ["LEVELS", "LOGGER_LEVELS", "LEVEL_NAMES", "LogRecord", "EventRecord", "Emission"]
|
|
12
|
+
|
|
13
|
+
LEVELS: Mapping[str, int] = MappingProxyType(
|
|
14
|
+
{"debug": 10, "verbose": 20, "normal": 30, "quiet": 40})
|
|
15
|
+
"""Verbosity name -> threshold ([X] OB3)."""
|
|
16
|
+
|
|
17
|
+
LOGGER_LEVELS: Mapping[str, int] = MappingProxyType(
|
|
18
|
+
{"debug": 10, "info": 30, "warning": 35, "error": 40})
|
|
19
|
+
"""Logger method -> level (DD-13); level 20 serves the runtime's own detailed messages."""
|
|
20
|
+
|
|
21
|
+
LEVEL_NAMES: Mapping[int, str] = MappingProxyType(
|
|
22
|
+
{10: "DEBUG", 20: "VERBOSE", 30: "INFO", 35: "WARNING", 40: "ERROR"})
|
|
23
|
+
"""Level -> the label written in the journals."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class LogRecord:
|
|
28
|
+
"""One message for humans ([X] §17.2).
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
run : str
|
|
33
|
+
The run directory name.
|
|
34
|
+
seq : int
|
|
35
|
+
Rank of the record in the run's observation streams.
|
|
36
|
+
wall : float
|
|
37
|
+
Wall-clock time (seconds since the epoch) at which the message was logged.
|
|
38
|
+
level : int
|
|
39
|
+
Its level (``LOGGER_LEVELS``, or 20 for the runtime's detail).
|
|
40
|
+
path : str
|
|
41
|
+
Log path of the component, e.g. ``run_017/subpop.proies/method.DEBoG/mutation``.
|
|
42
|
+
message : str
|
|
43
|
+
The formatted message.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
run: str
|
|
47
|
+
seq: int
|
|
48
|
+
wall: float
|
|
49
|
+
level: int
|
|
50
|
+
path: str
|
|
51
|
+
message: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class EventRecord:
|
|
56
|
+
"""One structured event ([X] §17.2).
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
run : str
|
|
61
|
+
The run directory name.
|
|
62
|
+
seq : int
|
|
63
|
+
Rank of the record in the run's observation streams.
|
|
64
|
+
wall : float
|
|
65
|
+
Wall-clock time of the emission.
|
|
66
|
+
path : str
|
|
67
|
+
Path of the emitting component.
|
|
68
|
+
name : str
|
|
69
|
+
Event name.
|
|
70
|
+
run_generation : int or None
|
|
71
|
+
Run generation at the emission.
|
|
72
|
+
date : object or None
|
|
73
|
+
Date in the calendar's unit.
|
|
74
|
+
fields : mapping of str to object
|
|
75
|
+
Plain data.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
run: str
|
|
79
|
+
seq: int
|
|
80
|
+
wall: float
|
|
81
|
+
path: str
|
|
82
|
+
name: str
|
|
83
|
+
run_generation: int | None
|
|
84
|
+
date: object | None
|
|
85
|
+
fields: Mapping[str, object]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class Emission:
|
|
90
|
+
"""One ``ctx.emit`` call, before the readout classifies it (design §3.6).
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
origin : {"system", "method"}
|
|
95
|
+
Kind of the emitting module.
|
|
96
|
+
module : str
|
|
97
|
+
Module name.
|
|
98
|
+
subpop : str or None
|
|
99
|
+
Subpopulation path, for method emissions.
|
|
100
|
+
path : str
|
|
101
|
+
Log path of the emitting component.
|
|
102
|
+
name : str
|
|
103
|
+
The name as emitted (before the ``method.`` prefix, [K] LG3).
|
|
104
|
+
fields : mapping of str to object
|
|
105
|
+
The keyword fields of the call.
|
|
106
|
+
run_generation : int or None
|
|
107
|
+
Run generation at the emission.
|
|
108
|
+
date : object
|
|
109
|
+
Date at the emission.
|
|
110
|
+
request_ranks : tuple of int or None
|
|
111
|
+
For an evaluate batch: candidate index -> request rank (trace attribution).
|
|
112
|
+
wall : float
|
|
113
|
+
Wall-clock time of the call.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
origin: Literal["system", "method"]
|
|
117
|
+
module: str
|
|
118
|
+
subpop: str | None
|
|
119
|
+
path: str
|
|
120
|
+
name: str
|
|
121
|
+
fields: Mapping[str, object]
|
|
122
|
+
run_generation: int | None
|
|
123
|
+
date: object
|
|
124
|
+
request_ranks: tuple[int, ...] | None
|
|
125
|
+
wall: float
|