-
-
Notifications
You must be signed in to change notification settings - Fork 764
Expand file tree
/
Copy pathtask_stream.py
More file actions
322 lines (265 loc) · 9.66 KB
/
Copy pathtask_stream.py
File metadata and controls
322 lines (265 loc) · 9.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
from __future__ import annotations
import logging
from collections import deque
from typing import Literal
import dask
from dask.utils import format_time, key_split, parse_timedelta
from distributed.client import Client, default_client
from distributed.diagnostics.plugin import SchedulerPlugin
from distributed.diagnostics.progress_stream import color_of
from distributed.metrics import time
logger = logging.getLogger(__name__)
class TaskStreamPlugin(SchedulerPlugin):
name = "task-stream"
def __init__(self, scheduler, maxlen=None):
if maxlen is None:
maxlen = max(
dask.config.get(
"distributed.scheduler.dashboard.status.task-stream-length"
),
dask.config.get(
"distributed.scheduler.dashboard.tasks.task-stream-length"
),
)
self.buffer = deque(maxlen=maxlen)
self.scheduler = scheduler
self.index = 0
def transition(self, key, start, finish, *args, **kwargs):
if start == "processing" and finish in ("memory", "erred"):
assert kwargs["startstops"]
kwargs["key"] = key
self.buffer.append(kwargs)
self.index += 1
def collect(self, start=None, stop=None, count=None, start_index=None):
# ``start_index`` selects records by their position in the monotonically
# increasing append counter (``self.index``) rather than by wall-clock
# time. This is immune to clock differences and latency between the
# client and the workers, which can otherwise cause time-based ``start``
# boundaries to drop tasks that have already completed.
if start_index is not None:
buffer_start = start_index - (self.index - len(self.buffer))
buffer_start = max(0, min(buffer_start, len(self.buffer)))
return [self.buffer[i] for i in range(buffer_start, len(self.buffer))]
def bisect(target, left, right):
while left != right:
mid = (left + right) // 2
stop = max(
startstop["stop"] for startstop in self.buffer[mid]["startstops"]
)
if stop < target:
left = mid + 1
else:
right = mid
return left
if isinstance(start, str):
start = time() - parse_timedelta(start)
if start is not None:
start = bisect(start, 0, len(self.buffer))
if isinstance(stop, str):
stop = time() - parse_timedelta(stop)
if stop is not None:
stop = bisect(stop, 0, len(self.buffer))
if count is not None:
if start is None and stop is None:
stop = len(self.buffer)
start = stop - count
elif start is None and stop is not None:
start = stop - count
elif start is not None and stop is None:
stop = start + count
if stop is None:
stop = len(self.buffer)
if start is None:
start = 0
start = max(0, start)
stop = min(stop, len(self.buffer))
return [self.buffer[i] for i in range(start, stop)]
def rectangles(self, istart, istop=None, workers=None, start_boundary=0):
msgs = []
diff = self.index - len(self.buffer)
if istop is None:
istop = self.index
for i in range(max(0, (istart or 0) - diff), istop - diff if istop else istop):
msg = self.buffer[i]
msgs.append(msg)
return rectangles(msgs, workers=workers, start_boundary=start_boundary)
def rectangles(msgs, workers=None, start_boundary=0):
if workers is None:
workers = {}
L_start = []
L_duration = []
L_duration_text = []
L_key = []
L_name = []
L_color = []
L_alpha = []
L_worker = []
L_worker_thread = []
L_y = []
for msg in msgs:
key = msg["key"]
name = key_split(key)
startstops = msg.get("startstops", [])
try:
worker_thread = f"{msg['worker']}-{msg['thread']}"
except Exception:
continue
if worker_thread not in workers:
workers[worker_thread] = len(workers) / 2
for startstop in startstops:
if startstop["start"] < start_boundary:
continue
color = colors[startstop["action"]]
if type(color) is not str:
color = color(msg)
L_start.append((startstop["start"] + startstop["stop"]) / 2 * 1000)
L_duration.append(1000 * (startstop["stop"] - startstop["start"]))
L_duration_text.append(format_time(startstop["stop"] - startstop["start"]))
L_key.append(key)
L_name.append(prefix[startstop["action"]] + name)
L_color.append(color)
L_alpha.append(alphas[startstop["action"]])
L_worker.append(msg["worker"])
L_worker_thread.append(worker_thread)
L_y.append(workers[worker_thread])
return {
"start": L_start,
"duration": L_duration,
"duration_text": L_duration_text,
"key": L_key,
"name": L_name,
"color": L_color,
"alpha": L_alpha,
"worker": L_worker,
"worker_thread": L_worker_thread,
"y": L_y,
}
def color_of_message(msg):
if msg["status"] == "OK":
split = key_split(msg["key"])
return color_of(split)
else:
return "black"
colors = {
"transfer": "red",
"disk-write": "orange",
"disk-read": "orange",
"deserialize": "gray",
"compute": color_of_message,
}
alphas = {
"transfer": 0.4,
"compute": 1,
"deserialize": 0.4,
"disk-write": 0.4,
"disk-read": 0.4,
}
prefix = {
"transfer": "transfer-",
"disk-write": "disk-write-",
"disk-read": "disk-read-",
"deserialize": "deserialize-",
"compute": "",
}
async def _get_task_stream_impl(
client,
start=None,
stop=None,
count=None,
plot=False,
filename="task-stream.html",
bokeh_resources=None,
start_index=None,
):
"""Asynchronous implementation of Client.get_task_stream and of the
get_task_stream context manager
"""
msgs = await client.scheduler.get_task_stream(
start=start, stop=stop, count=count, start_index=start_index
)
if not plot:
return msgs
from distributed.dashboard.components.scheduler import task_stream_figure
rects = rectangles(msgs)
source, figure = task_stream_figure(sizing_mode="stretch_both")
source.data.update(rects)
if plot == "save":
from bokeh.plotting import output_file, save
output_file(filename=filename, title="Dask Task Stream")
save(figure, filename=filename, resources=bokeh_resources)
return (msgs, figure)
class get_task_stream:
"""
Collect task stream within a context block
This provides diagnostic information about every task that was run during
the time when this block was active.
This must be used as a context manager.
Parameters
----------
plot: boolean, str
If true then also return a Bokeh figure
If plot == 'save' then save the figure to a file
filename: str (optional)
The filename to save to if you set ``plot='save'``
Examples
--------
>>> with get_task_stream() as ts:
... x.compute()
>>> ts.data
[...]
Get back a Bokeh figure and optionally save to a file
>>> with get_task_stream(plot='save', filename='task-stream.html') as ts:
... x.compute()
>>> ts.figure
<Bokeh Figure>
To share this file with others you may wish to upload and serve it online.
A common way to do this is to upload the file as a gist, and then serve it
on https://raw.githack.com ::
$ python -m pip install gist
$ gist task-stream.html
https://gist.github.com/8a5b3c74b10b413f612bb5e250856ceb
You can then navigate to that site, click the "Raw" button to the right of
the ``task-stream.html`` file, and then provide that URL to
https://raw.githack.com . This process should provide a sharable link that
others can use to see your task stream plot.
See Also
--------
Client.get_task_stream: Function version of this context manager
"""
data: list[dict]
def __init__(
self,
client: Client | None = None,
plot: bool | Literal["save"] = False,
filename: str = "task-stream.html",
):
self.data = []
self._plot = plot
self._filename = filename
self.figure = None
self.client = client or default_client()
self._start_index = None
def __enter__(self):
return self.client.sync(self.__aenter__)
def __exit__(self, exc_type, exc_value, traceback):
return self.client.sync(self.__aexit__, exc_type, exc_value, traceback)
async def __aenter__(self):
"""Record the scheduler's task-stream cursor on entry and collect
everything appended after it on exit. Using the monotonic index
instead of a wall-clock boundary avoids dropping tasks when there is
latency or clock skew between the client and the workers.
"""
self._start_index = await self.client.scheduler.get_task_stream_index()
return self
async def __aexit__(self, exc_type, exc_value, traceback):
res = await _get_task_stream_impl(
self.client,
start_index=self._start_index,
plot=self._plot,
filename=self._filename,
)
if self._plot:
data, self.figure = res
else:
data = res
self.data.extend(data)