opengris-scaler 1.12.37__cp38-cp38-musllinux_1_2_x86_64.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.
- opengris_scaler-1.12.37.dist-info/METADATA +730 -0
- opengris_scaler-1.12.37.dist-info/RECORD +196 -0
- opengris_scaler-1.12.37.dist-info/WHEEL +5 -0
- opengris_scaler-1.12.37.dist-info/entry_points.txt +10 -0
- opengris_scaler-1.12.37.dist-info/licenses/LICENSE +201 -0
- opengris_scaler-1.12.37.dist-info/licenses/LICENSE.spdx +7 -0
- opengris_scaler-1.12.37.dist-info/licenses/NOTICE +8 -0
- opengris_scaler.libs/libcapnp-1-e88d5415.0.1.so +0 -0
- opengris_scaler.libs/libgcc_s-2298274a.so.1 +0 -0
- opengris_scaler.libs/libkj-1-9bebd8ac.0.1.so +0 -0
- opengris_scaler.libs/libstdc++-08d5c7eb.so.6.0.33 +0 -0
- scaler/__init__.py +14 -0
- scaler/about.py +5 -0
- scaler/client/__init__.py +0 -0
- scaler/client/agent/__init__.py +0 -0
- scaler/client/agent/client_agent.py +218 -0
- scaler/client/agent/disconnect_manager.py +27 -0
- scaler/client/agent/future_manager.py +112 -0
- scaler/client/agent/heartbeat_manager.py +74 -0
- scaler/client/agent/mixins.py +89 -0
- scaler/client/agent/object_manager.py +98 -0
- scaler/client/agent/task_manager.py +64 -0
- scaler/client/client.py +672 -0
- scaler/client/future.py +252 -0
- scaler/client/object_buffer.py +129 -0
- scaler/client/object_reference.py +25 -0
- scaler/client/serializer/__init__.py +0 -0
- scaler/client/serializer/default.py +16 -0
- scaler/client/serializer/mixins.py +38 -0
- scaler/cluster/__init__.py +0 -0
- scaler/cluster/cluster.py +95 -0
- scaler/cluster/combo.py +157 -0
- scaler/cluster/object_storage_server.py +45 -0
- scaler/cluster/scheduler.py +86 -0
- scaler/config/__init__.py +0 -0
- scaler/config/common/__init__.py +0 -0
- scaler/config/common/logging.py +41 -0
- scaler/config/common/web.py +18 -0
- scaler/config/common/worker.py +65 -0
- scaler/config/common/worker_adapter.py +28 -0
- scaler/config/config_class.py +317 -0
- scaler/config/defaults.py +94 -0
- scaler/config/mixins.py +20 -0
- scaler/config/section/__init__.py +0 -0
- scaler/config/section/cluster.py +66 -0
- scaler/config/section/ecs_worker_adapter.py +78 -0
- scaler/config/section/native_worker_adapter.py +30 -0
- scaler/config/section/object_storage_server.py +13 -0
- scaler/config/section/scheduler.py +126 -0
- scaler/config/section/symphony_worker_adapter.py +35 -0
- scaler/config/section/top.py +16 -0
- scaler/config/section/webui.py +16 -0
- scaler/config/types/__init__.py +0 -0
- scaler/config/types/network_backend.py +12 -0
- scaler/config/types/object_storage_server.py +45 -0
- scaler/config/types/worker.py +67 -0
- scaler/config/types/zmq.py +83 -0
- scaler/entry_points/__init__.py +0 -0
- scaler/entry_points/cluster.py +10 -0
- scaler/entry_points/object_storage_server.py +26 -0
- scaler/entry_points/scheduler.py +51 -0
- scaler/entry_points/top.py +272 -0
- scaler/entry_points/webui.py +6 -0
- scaler/entry_points/worker_adapter_ecs.py +22 -0
- scaler/entry_points/worker_adapter_native.py +31 -0
- scaler/entry_points/worker_adapter_symphony.py +26 -0
- scaler/io/__init__.py +0 -0
- scaler/io/async_binder.py +89 -0
- scaler/io/async_connector.py +95 -0
- scaler/io/async_object_storage_connector.py +225 -0
- scaler/io/mixins.py +154 -0
- scaler/io/sync_connector.py +68 -0
- scaler/io/sync_object_storage_connector.py +249 -0
- scaler/io/sync_subscriber.py +83 -0
- scaler/io/utility.py +80 -0
- scaler/io/ymq/__init__.py +0 -0
- scaler/io/ymq/_ymq.pyi +95 -0
- scaler/io/ymq/_ymq.so +0 -0
- scaler/io/ymq/ymq.py +138 -0
- scaler/io/ymq_async_object_storage_connector.py +184 -0
- scaler/io/ymq_sync_object_storage_connector.py +184 -0
- scaler/object_storage/__init__.py +0 -0
- scaler/object_storage/object_storage_server.so +0 -0
- scaler/protocol/__init__.py +0 -0
- scaler/protocol/capnp/__init__.py +0 -0
- scaler/protocol/capnp/_python.py +6 -0
- scaler/protocol/capnp/common.capnp +68 -0
- scaler/protocol/capnp/message.capnp +218 -0
- scaler/protocol/capnp/object_storage.capnp +57 -0
- scaler/protocol/capnp/status.capnp +73 -0
- scaler/protocol/introduction.md +105 -0
- scaler/protocol/python/__init__.py +0 -0
- scaler/protocol/python/common.py +140 -0
- scaler/protocol/python/message.py +751 -0
- scaler/protocol/python/mixins.py +13 -0
- scaler/protocol/python/object_storage.py +118 -0
- scaler/protocol/python/status.py +279 -0
- scaler/protocol/worker.md +228 -0
- scaler/scheduler/__init__.py +0 -0
- scaler/scheduler/allocate_policy/__init__.py +0 -0
- scaler/scheduler/allocate_policy/allocate_policy.py +9 -0
- scaler/scheduler/allocate_policy/capability_allocate_policy.py +280 -0
- scaler/scheduler/allocate_policy/even_load_allocate_policy.py +159 -0
- scaler/scheduler/allocate_policy/mixins.py +55 -0
- scaler/scheduler/controllers/__init__.py +0 -0
- scaler/scheduler/controllers/balance_controller.py +65 -0
- scaler/scheduler/controllers/client_controller.py +131 -0
- scaler/scheduler/controllers/config_controller.py +31 -0
- scaler/scheduler/controllers/graph_controller.py +424 -0
- scaler/scheduler/controllers/information_controller.py +81 -0
- scaler/scheduler/controllers/mixins.py +194 -0
- scaler/scheduler/controllers/object_controller.py +147 -0
- scaler/scheduler/controllers/scaling_policies/__init__.py +0 -0
- scaler/scheduler/controllers/scaling_policies/fixed_elastic.py +145 -0
- scaler/scheduler/controllers/scaling_policies/mixins.py +10 -0
- scaler/scheduler/controllers/scaling_policies/null.py +14 -0
- scaler/scheduler/controllers/scaling_policies/types.py +9 -0
- scaler/scheduler/controllers/scaling_policies/utility.py +20 -0
- scaler/scheduler/controllers/scaling_policies/vanilla.py +95 -0
- scaler/scheduler/controllers/task_controller.py +376 -0
- scaler/scheduler/controllers/worker_controller.py +169 -0
- scaler/scheduler/object_usage/__init__.py +0 -0
- scaler/scheduler/object_usage/object_tracker.py +131 -0
- scaler/scheduler/scheduler.py +251 -0
- scaler/scheduler/task/__init__.py +0 -0
- scaler/scheduler/task/task_state_machine.py +92 -0
- scaler/scheduler/task/task_state_manager.py +61 -0
- scaler/ui/__init__.py +0 -0
- scaler/ui/common/__init__.py +0 -0
- scaler/ui/common/constants.py +9 -0
- scaler/ui/common/live_display.py +147 -0
- scaler/ui/common/memory_window.py +146 -0
- scaler/ui/common/setting_page.py +40 -0
- scaler/ui/common/task_graph.py +840 -0
- scaler/ui/common/task_log.py +111 -0
- scaler/ui/common/utility.py +66 -0
- scaler/ui/common/webui.py +80 -0
- scaler/ui/common/worker_processors.py +104 -0
- scaler/ui/v1.py +76 -0
- scaler/ui/v2.py +102 -0
- scaler/ui/webui.py +21 -0
- scaler/utility/__init__.py +0 -0
- scaler/utility/debug.py +19 -0
- scaler/utility/event_list.py +63 -0
- scaler/utility/event_loop.py +58 -0
- scaler/utility/exceptions.py +42 -0
- scaler/utility/formatter.py +44 -0
- scaler/utility/graph/__init__.py +0 -0
- scaler/utility/graph/optimization.py +27 -0
- scaler/utility/graph/topological_sorter.py +11 -0
- scaler/utility/graph/topological_sorter_graphblas.py +174 -0
- scaler/utility/identifiers.py +107 -0
- scaler/utility/logging/__init__.py +0 -0
- scaler/utility/logging/decorators.py +25 -0
- scaler/utility/logging/scoped_logger.py +33 -0
- scaler/utility/logging/utility.py +183 -0
- scaler/utility/many_to_many_dict.py +123 -0
- scaler/utility/metadata/__init__.py +0 -0
- scaler/utility/metadata/profile_result.py +31 -0
- scaler/utility/metadata/task_flags.py +30 -0
- scaler/utility/mixins.py +13 -0
- scaler/utility/network_util.py +7 -0
- scaler/utility/one_to_many_dict.py +72 -0
- scaler/utility/queues/__init__.py +0 -0
- scaler/utility/queues/async_indexed_queue.py +37 -0
- scaler/utility/queues/async_priority_queue.py +70 -0
- scaler/utility/queues/async_sorted_priority_queue.py +45 -0
- scaler/utility/queues/indexed_queue.py +114 -0
- scaler/utility/serialization.py +9 -0
- scaler/version.txt +1 -0
- scaler/worker/__init__.py +0 -0
- scaler/worker/agent/__init__.py +0 -0
- scaler/worker/agent/heartbeat_manager.py +110 -0
- scaler/worker/agent/mixins.py +137 -0
- scaler/worker/agent/processor/__init__.py +0 -0
- scaler/worker/agent/processor/object_cache.py +107 -0
- scaler/worker/agent/processor/processor.py +285 -0
- scaler/worker/agent/processor/streaming_buffer.py +28 -0
- scaler/worker/agent/processor_holder.py +147 -0
- scaler/worker/agent/processor_manager.py +369 -0
- scaler/worker/agent/profiling_manager.py +109 -0
- scaler/worker/agent/task_manager.py +150 -0
- scaler/worker/agent/timeout_manager.py +19 -0
- scaler/worker/preload.py +84 -0
- scaler/worker/worker.py +265 -0
- scaler/worker_adapter/__init__.py +0 -0
- scaler/worker_adapter/common.py +26 -0
- scaler/worker_adapter/ecs.py +241 -0
- scaler/worker_adapter/native.py +138 -0
- scaler/worker_adapter/symphony/__init__.py +0 -0
- scaler/worker_adapter/symphony/callback.py +45 -0
- scaler/worker_adapter/symphony/heartbeat_manager.py +82 -0
- scaler/worker_adapter/symphony/message.py +24 -0
- scaler/worker_adapter/symphony/task_manager.py +289 -0
- scaler/worker_adapter/symphony/worker.py +204 -0
- scaler/worker_adapter/symphony/worker_adapter.py +123 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import logging
|
|
4
|
+
from collections import deque
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import Deque, Dict, List, Optional, Set, Tuple
|
|
8
|
+
|
|
9
|
+
from nicegui import ui
|
|
10
|
+
|
|
11
|
+
from scaler.protocol.python.common import TaskState, WorkerState
|
|
12
|
+
from scaler.protocol.python.message import StateTask, StateWorker
|
|
13
|
+
from scaler.ui.common.setting_page import Settings
|
|
14
|
+
from scaler.ui.common.utility import (
|
|
15
|
+
COMPLETED_TASK_STATUSES,
|
|
16
|
+
display_capabilities,
|
|
17
|
+
format_timediff,
|
|
18
|
+
format_worker_name,
|
|
19
|
+
get_bounds,
|
|
20
|
+
make_taskstream_ticks,
|
|
21
|
+
make_tick_text,
|
|
22
|
+
)
|
|
23
|
+
from scaler.ui.util import NICEGUI_MAJOR_VERSION
|
|
24
|
+
|
|
25
|
+
TASK_STREAM_BACKGROUND_COLOR = "white"
|
|
26
|
+
TASK_STREAM_BACKGROUND_COLOR_RGB = "#000000"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TaskShape(Enum):
|
|
30
|
+
NONE = ""
|
|
31
|
+
FAILED = "x"
|
|
32
|
+
CANCELED = "/"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TaskShapes:
|
|
36
|
+
_task_status_to_shape = {
|
|
37
|
+
TaskState.Success: TaskShape.NONE.value,
|
|
38
|
+
TaskState.Running: TaskShape.NONE.value,
|
|
39
|
+
TaskState.Failed: TaskShape.FAILED.value,
|
|
40
|
+
TaskState.FailedWorkerDied: TaskShape.FAILED.value,
|
|
41
|
+
TaskState.Canceled: TaskShape.CANCELED.value,
|
|
42
|
+
TaskState.CanceledNotFound: TaskShape.CANCELED.value,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
_task_shape_to_outline = {
|
|
46
|
+
TaskState.Success: ("black", 1),
|
|
47
|
+
TaskState.Running: ("yellow", 1),
|
|
48
|
+
TaskState.Failed: ("red", 1),
|
|
49
|
+
TaskState.FailedWorkerDied: ("red", 1),
|
|
50
|
+
TaskState.Canceled: ("black", 1),
|
|
51
|
+
TaskState.CanceledNotFound: ("black", 1),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_status(cls, status: TaskState) -> str:
|
|
56
|
+
return cls._task_status_to_shape[status]
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_outline(cls, status: TaskState) -> Tuple[str, int]:
|
|
60
|
+
return cls._task_shape_to_outline[status]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TaskStream:
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self._card: Optional[ui.card] = None
|
|
66
|
+
self._figure = {}
|
|
67
|
+
self._plot = None
|
|
68
|
+
|
|
69
|
+
self._settings: Optional[Settings] = None
|
|
70
|
+
|
|
71
|
+
self._start_time = datetime.datetime.now() - datetime.timedelta(minutes=30)
|
|
72
|
+
self._user_axis_range: Optional[List[int]] = None
|
|
73
|
+
|
|
74
|
+
self._current_tasks: Dict[str, Dict[bytes, datetime.datetime]] = {}
|
|
75
|
+
self._completed_data_cache: Dict[str, Dict] = {}
|
|
76
|
+
|
|
77
|
+
self._worker_to_object_name: Dict[str, str] = {}
|
|
78
|
+
self._worker_to_task_ids: Dict[str, Set[bytes]] = {}
|
|
79
|
+
self._worker_capabilities: Dict[str, Set[str]] = {}
|
|
80
|
+
self._task_id_to_worker: Dict[bytes, str] = {}
|
|
81
|
+
self._task_id_to_printable_capabilities: Dict[bytes, str] = {}
|
|
82
|
+
|
|
83
|
+
self._seen_workers = set()
|
|
84
|
+
|
|
85
|
+
self._data_update_lock = Lock()
|
|
86
|
+
|
|
87
|
+
self._dead_workers: Deque[Tuple[datetime.datetime, str]] = deque() # type: ignore[misc]
|
|
88
|
+
|
|
89
|
+
self._capabilities_color_map: Dict[str, str] = {"<no capabilities>": "green"}
|
|
90
|
+
|
|
91
|
+
self._worker_rows: Dict[str, List[str]] = {}
|
|
92
|
+
self._row_to_worker: Dict[str, str] = {}
|
|
93
|
+
self._task_row_assignment: Dict[bytes, str] = {}
|
|
94
|
+
self._row_last_used: Dict[str, datetime.datetime] = {}
|
|
95
|
+
|
|
96
|
+
def __set_worker_rows(self, worker: str, rows: List[str]):
|
|
97
|
+
self._worker_rows[worker] = rows
|
|
98
|
+
for row in rows:
|
|
99
|
+
self._row_to_worker[row] = worker
|
|
100
|
+
|
|
101
|
+
def __add_worker_row(self, worker: str, row: str):
|
|
102
|
+
self._worker_rows[worker].append(row)
|
|
103
|
+
self._row_to_worker[row] = worker
|
|
104
|
+
|
|
105
|
+
def __clear_worker_rows(self, worker: str) -> List[str]:
|
|
106
|
+
rows = self._worker_rows.pop(worker, [])
|
|
107
|
+
for row in rows:
|
|
108
|
+
self._row_to_worker.pop(row, None)
|
|
109
|
+
self._row_last_used.pop(row, None)
|
|
110
|
+
return rows
|
|
111
|
+
|
|
112
|
+
def __make_initial_row(self, worker: str):
|
|
113
|
+
base = format_worker_name(worker)
|
|
114
|
+
rows = self._worker_rows.get(worker)
|
|
115
|
+
if rows is None:
|
|
116
|
+
# first row is [1]
|
|
117
|
+
rows = [f"{base} [1]"]
|
|
118
|
+
self.__set_worker_rows(worker, rows)
|
|
119
|
+
|
|
120
|
+
def __allocate_row_for_task(self, worker: str, task_id: bytes) -> str:
|
|
121
|
+
if task_id in self._task_row_assignment:
|
|
122
|
+
return self._task_row_assignment[task_id]
|
|
123
|
+
|
|
124
|
+
if worker not in self._worker_rows:
|
|
125
|
+
self.__make_initial_row(worker)
|
|
126
|
+
|
|
127
|
+
# pick first unused row label
|
|
128
|
+
used_rows = set(self._task_row_assignment.values())
|
|
129
|
+
for row in self._worker_rows[worker]:
|
|
130
|
+
if row not in used_rows:
|
|
131
|
+
self._task_row_assignment[task_id] = row
|
|
132
|
+
|
|
133
|
+
if row not in self._completed_data_cache:
|
|
134
|
+
self.__setup_row_cache(row)
|
|
135
|
+
row_data = self._completed_data_cache.get(row)
|
|
136
|
+
if row_data is not None and row not in row_data.get("y", []):
|
|
137
|
+
now = datetime.datetime.now()
|
|
138
|
+
self.__add_bar(
|
|
139
|
+
worker=worker,
|
|
140
|
+
time_taken=format_timediff(self._start_time, now),
|
|
141
|
+
task_color=TASK_STREAM_BACKGROUND_COLOR,
|
|
142
|
+
state=None,
|
|
143
|
+
hovertext="",
|
|
144
|
+
capabilities_display_string="",
|
|
145
|
+
row_label=row,
|
|
146
|
+
)
|
|
147
|
+
self._row_last_used[row] = now
|
|
148
|
+
|
|
149
|
+
return row
|
|
150
|
+
|
|
151
|
+
# need to add a new row
|
|
152
|
+
new_row = f"{format_worker_name(worker)} [{len(self._worker_rows[worker]) + 1}]"
|
|
153
|
+
self.__add_worker_row(worker, new_row)
|
|
154
|
+
self._task_row_assignment[task_id] = new_row
|
|
155
|
+
|
|
156
|
+
self.__setup_row_cache(new_row)
|
|
157
|
+
row_data = self._completed_data_cache.get(new_row)
|
|
158
|
+
if row_data is not None and new_row not in row_data.get("y", []):
|
|
159
|
+
now = datetime.datetime.now()
|
|
160
|
+
self.__add_bar(
|
|
161
|
+
worker=worker,
|
|
162
|
+
time_taken=format_timediff(self._start_time, now),
|
|
163
|
+
task_color=TASK_STREAM_BACKGROUND_COLOR,
|
|
164
|
+
state=None,
|
|
165
|
+
hovertext="",
|
|
166
|
+
capabilities_display_string="",
|
|
167
|
+
row_label=new_row,
|
|
168
|
+
)
|
|
169
|
+
self._row_last_used[new_row] = now
|
|
170
|
+
|
|
171
|
+
return new_row
|
|
172
|
+
|
|
173
|
+
def __free_row_for_task(self, task_id: bytes):
|
|
174
|
+
if task_id in self._task_row_assignment:
|
|
175
|
+
self._task_row_assignment.pop(task_id)
|
|
176
|
+
|
|
177
|
+
def setup_task_stream(self, settings: Settings):
|
|
178
|
+
self._card = ui.card()
|
|
179
|
+
self._card.classes("w-full").style("height: 800px; overflow:auto;")
|
|
180
|
+
|
|
181
|
+
# TODO: remove when v1 and v2 are separated
|
|
182
|
+
def html_func(x: str):
|
|
183
|
+
if NICEGUI_MAJOR_VERSION < 3:
|
|
184
|
+
return ui.html(x)
|
|
185
|
+
return ui.html(x, sanitize=False) # type: ignore[call-arg]
|
|
186
|
+
|
|
187
|
+
with self._card:
|
|
188
|
+
html_func(
|
|
189
|
+
"""
|
|
190
|
+
<div style="margin-bottom:8px;">
|
|
191
|
+
<b>Legend:</b>
|
|
192
|
+
<span style="display:inline-block;width:18px;height:18px;border:2px solid black;
|
|
193
|
+
vertical-align:middle;margin-right:4px;
|
|
194
|
+
background:
|
|
195
|
+
repeating-linear-gradient(135deg, transparent, transparent 4px, black 4px, black 6px),
|
|
196
|
+
repeating-linear-gradient(225deg, transparent, transparent 4px, black 4px, black 6px);
|
|
197
|
+
"></span>
|
|
198
|
+
Failed
|
|
199
|
+
<span style="display:inline-block;width:18px;height:18px;border:2px solid black;
|
|
200
|
+
vertical-align:middle;margin-left:16px;margin-right:4px;
|
|
201
|
+
background:
|
|
202
|
+
repeating-linear-gradient(-45deg, transparent, transparent 4px, black 4px, black 6px);
|
|
203
|
+
"></span>
|
|
204
|
+
Canceled
|
|
205
|
+
</div>
|
|
206
|
+
"""
|
|
207
|
+
)
|
|
208
|
+
fig = {
|
|
209
|
+
"data": [],
|
|
210
|
+
"layout": {
|
|
211
|
+
"barmode": "stack",
|
|
212
|
+
"autosize": True,
|
|
213
|
+
"margin": {"l": 163},
|
|
214
|
+
"xaxis": {
|
|
215
|
+
"autorange": False,
|
|
216
|
+
"range": [0, 300],
|
|
217
|
+
"showgrid": False,
|
|
218
|
+
"tickmode": "array",
|
|
219
|
+
"tickvals": [0, 50, 100, 150, 200, 250, 300],
|
|
220
|
+
"ticktext": [-300, -250, -200, -150, -100, -50, 0],
|
|
221
|
+
"zeroline": False,
|
|
222
|
+
},
|
|
223
|
+
"yaxis": {
|
|
224
|
+
"autorange": True,
|
|
225
|
+
"automargin": True,
|
|
226
|
+
"rangemode": "nonnegative",
|
|
227
|
+
"showgrid": False,
|
|
228
|
+
"type": "category",
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
self._figure = fig
|
|
233
|
+
self._completed_data_cache = {}
|
|
234
|
+
self._plot = ui.plotly(self._figure).classes("w-full h-full")
|
|
235
|
+
self._plot.on("plotly_relayout", self._on_plotly_relayout)
|
|
236
|
+
self._settings = settings
|
|
237
|
+
|
|
238
|
+
def _on_plotly_relayout(self, e):
|
|
239
|
+
x0 = e.args.get("xaxis.range[0]")
|
|
240
|
+
x1 = e.args.get("xaxis.range[1]")
|
|
241
|
+
if x0 is not None and x1 is not None:
|
|
242
|
+
self._user_axis_range = [x0, x1]
|
|
243
|
+
else:
|
|
244
|
+
self._user_axis_range = None
|
|
245
|
+
|
|
246
|
+
def __setup_row_cache(self, row_label: str):
|
|
247
|
+
if row_label in self._completed_data_cache:
|
|
248
|
+
return
|
|
249
|
+
self._completed_data_cache[row_label] = {
|
|
250
|
+
"type": "bar",
|
|
251
|
+
"name": "Completed",
|
|
252
|
+
"y": [],
|
|
253
|
+
"x": [],
|
|
254
|
+
"orientation": "h",
|
|
255
|
+
"marker": {"color": [], "width": 5, "pattern": {"shape": []}, "line": {"color": [], "width": []}},
|
|
256
|
+
"hovertemplate": [],
|
|
257
|
+
"hovertext": [],
|
|
258
|
+
"customdata": [],
|
|
259
|
+
"showlegend": False,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
def __get_history_fields(self, worker: str, index: int) -> Tuple[float, str, str, str, str, str, int, str]:
|
|
263
|
+
row_data = self._completed_data_cache[worker]
|
|
264
|
+
time_taken = row_data["x"][index]
|
|
265
|
+
color = row_data["marker"]["color"][index]
|
|
266
|
+
text = row_data["hovertext"][index]
|
|
267
|
+
shape = row_data["marker"]["pattern"]["shape"][index]
|
|
268
|
+
capabilities = row_data["customdata"][index]["capabilities"]
|
|
269
|
+
row_label = row_data["y"][index]
|
|
270
|
+
outline_color = row_data["marker"]["line"]["color"][index]
|
|
271
|
+
outline_width = row_data["marker"]["line"]["width"][index]
|
|
272
|
+
return time_taken, color, text, shape, capabilities, row_label, outline_width, outline_color
|
|
273
|
+
|
|
274
|
+
def __remove_last_elements(self, row_label: str):
|
|
275
|
+
row_data = self._completed_data_cache[row_label]
|
|
276
|
+
del row_data["y"][-1]
|
|
277
|
+
del row_data["x"][-1]
|
|
278
|
+
del row_data["marker"]["color"][-1]
|
|
279
|
+
del row_data["marker"]["pattern"]["shape"][-1]
|
|
280
|
+
del row_data["marker"]["line"]["color"][-1]
|
|
281
|
+
del row_data["marker"]["line"]["width"][-1]
|
|
282
|
+
del row_data["hovertext"][-1]
|
|
283
|
+
del row_data["hovertemplate"][-1]
|
|
284
|
+
del row_data["customdata"][-1]
|
|
285
|
+
|
|
286
|
+
def __get_capabilities_color(self, capabilities: str) -> str:
|
|
287
|
+
if capabilities not in self._capabilities_color_map:
|
|
288
|
+
h = hashlib.md5(capabilities.encode()).hexdigest()
|
|
289
|
+
color = f"#{h[:6]}"
|
|
290
|
+
if color == TASK_STREAM_BACKGROUND_COLOR_RGB:
|
|
291
|
+
color = "#0000ff"
|
|
292
|
+
self._capabilities_color_map[capabilities] = color
|
|
293
|
+
return self._capabilities_color_map[capabilities]
|
|
294
|
+
|
|
295
|
+
def __get_task_color(self, task_id: bytes) -> str:
|
|
296
|
+
capabilities = self._task_id_to_printable_capabilities.get(task_id, "<no capabilities>")
|
|
297
|
+
color = self.__get_capabilities_color(capabilities)
|
|
298
|
+
return color
|
|
299
|
+
|
|
300
|
+
def __add_task_to_chart(self, worker: str, task_id: bytes, task_state: TaskState, task_time: float) -> str:
|
|
301
|
+
task_color = self.__get_task_color(task_id)
|
|
302
|
+
task_hovertext = self._worker_to_object_name.get(worker, "")
|
|
303
|
+
capabilities_display_string = self._task_id_to_printable_capabilities.get(task_id, "")
|
|
304
|
+
|
|
305
|
+
row_label = self._task_row_assignment.get(task_id)
|
|
306
|
+
|
|
307
|
+
self.__add_bar(
|
|
308
|
+
worker=worker,
|
|
309
|
+
time_taken=task_time,
|
|
310
|
+
task_color=task_color,
|
|
311
|
+
state=task_state,
|
|
312
|
+
hovertext=task_hovertext,
|
|
313
|
+
capabilities_display_string=capabilities_display_string,
|
|
314
|
+
row_label=row_label,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
self.__free_row_for_task(task_id)
|
|
318
|
+
|
|
319
|
+
return row_label
|
|
320
|
+
|
|
321
|
+
def __add_bar(
|
|
322
|
+
self,
|
|
323
|
+
worker: str,
|
|
324
|
+
time_taken: float,
|
|
325
|
+
task_color: str,
|
|
326
|
+
state: Optional[TaskState],
|
|
327
|
+
hovertext: str,
|
|
328
|
+
capabilities_display_string: str,
|
|
329
|
+
row_label: Optional[str] = None,
|
|
330
|
+
):
|
|
331
|
+
if row_label is None:
|
|
332
|
+
row_label = f"{format_worker_name(worker)} [1]"
|
|
333
|
+
|
|
334
|
+
if state:
|
|
335
|
+
shape = TaskShapes.from_status(state)
|
|
336
|
+
task_outline_color, task_outline_width = TaskShapes.get_outline(state)
|
|
337
|
+
state_text = state.name
|
|
338
|
+
else:
|
|
339
|
+
shape = TaskShape.NONE.value
|
|
340
|
+
task_outline_color, task_outline_width = ("rgba(0,0,0,0)", 0)
|
|
341
|
+
state_text = ""
|
|
342
|
+
row_history = self._completed_data_cache[row_label]
|
|
343
|
+
|
|
344
|
+
# if this is the same task repeated, merge the bars
|
|
345
|
+
if len(row_history["y"]) > 0:
|
|
346
|
+
(
|
|
347
|
+
last_time_taken,
|
|
348
|
+
last_color,
|
|
349
|
+
last_text,
|
|
350
|
+
last_shape,
|
|
351
|
+
last_capabilities,
|
|
352
|
+
last_row,
|
|
353
|
+
last_outline_width,
|
|
354
|
+
last_outline_color,
|
|
355
|
+
) = self.__get_history_fields(row_label, -1)
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
last_row == row_label
|
|
359
|
+
and last_color == task_color
|
|
360
|
+
and last_text == hovertext
|
|
361
|
+
and last_shape == shape
|
|
362
|
+
and last_capabilities == capabilities_display_string
|
|
363
|
+
and last_outline_width == task_outline_width
|
|
364
|
+
and last_outline_color == task_outline_color
|
|
365
|
+
):
|
|
366
|
+
row_history["x"][-1] += time_taken
|
|
367
|
+
row_history["customdata"][-1]["task_count"] += 1
|
|
368
|
+
return
|
|
369
|
+
|
|
370
|
+
# if there's a very short gap from last task to current task, merge the bars
|
|
371
|
+
if task_color != TASK_STREAM_BACKGROUND_COLOR and len(row_history["y"]) > 2:
|
|
372
|
+
(
|
|
373
|
+
_,
|
|
374
|
+
penult_color,
|
|
375
|
+
penult_text,
|
|
376
|
+
penult_shape,
|
|
377
|
+
penult_capabilities,
|
|
378
|
+
penult_row,
|
|
379
|
+
penult_outline_width,
|
|
380
|
+
penult_outline_color,
|
|
381
|
+
) = self.__get_history_fields(row_label, -2)
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
last_time_taken < 0.1
|
|
385
|
+
and penult_row == row_label
|
|
386
|
+
and penult_color == task_color
|
|
387
|
+
and penult_text == hovertext
|
|
388
|
+
and penult_shape == shape
|
|
389
|
+
and penult_capabilities == capabilities_display_string
|
|
390
|
+
and penult_outline_width == task_outline_width
|
|
391
|
+
and penult_outline_color == task_outline_color
|
|
392
|
+
):
|
|
393
|
+
row_history["x"][-2] += time_taken + last_time_taken
|
|
394
|
+
row_history["customdata"][-2]["task_count"] += 1
|
|
395
|
+
self.__remove_last_elements(row_label)
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
self._completed_data_cache[row_label]["y"].append(row_label)
|
|
399
|
+
self._completed_data_cache[row_label]["x"].append(time_taken)
|
|
400
|
+
self._completed_data_cache[row_label]["marker"]["color"].append(task_color)
|
|
401
|
+
self._completed_data_cache[row_label]["marker"]["pattern"]["shape"].append(shape)
|
|
402
|
+
self._completed_data_cache[row_label]["marker"]["line"]["color"].append(task_outline_color)
|
|
403
|
+
self._completed_data_cache[row_label]["marker"]["line"]["width"].append(task_outline_width)
|
|
404
|
+
self._completed_data_cache[row_label]["hovertext"].append(hovertext)
|
|
405
|
+
self._completed_data_cache[row_label]["customdata"].append(
|
|
406
|
+
{"state": state_text, "task_count": 1, "capabilities": capabilities_display_string}
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if hovertext:
|
|
410
|
+
self._completed_data_cache[row_label]["hovertemplate"].append(
|
|
411
|
+
"%{hovertext} (%{x})<br>"
|
|
412
|
+
"%{customdata.state}<br>"
|
|
413
|
+
"Tasks: %{customdata.task_count}<br>"
|
|
414
|
+
"Capabilities: %{customdata.capabilities}"
|
|
415
|
+
)
|
|
416
|
+
else:
|
|
417
|
+
self._completed_data_cache[row_label]["hovertemplate"].append("")
|
|
418
|
+
|
|
419
|
+
def __cutoff_keep_first(self, data_list: list, cutoff_index: int):
|
|
420
|
+
return [data_list[0]] + data_list[cutoff_index:]
|
|
421
|
+
|
|
422
|
+
def __remove_old_tasks_from_cache(self, row_label: str, cutoff_index: int):
|
|
423
|
+
row_data = self._completed_data_cache[row_label]
|
|
424
|
+
removed_time = sum([row_data["x"][i] for i in range(1, cutoff_index)])
|
|
425
|
+
|
|
426
|
+
row_data["y"] = self.__cutoff_keep_first(row_data["y"], cutoff_index)
|
|
427
|
+
row_data["x"] = self.__cutoff_keep_first(row_data["x"], cutoff_index)
|
|
428
|
+
row_data["marker"]["color"] = self.__cutoff_keep_first(row_data["marker"]["color"], cutoff_index)
|
|
429
|
+
row_data["marker"]["pattern"]["shape"] = self.__cutoff_keep_first(
|
|
430
|
+
row_data["marker"]["pattern"]["shape"], cutoff_index
|
|
431
|
+
)
|
|
432
|
+
row_data["marker"]["line"]["color"] = self.__cutoff_keep_first(
|
|
433
|
+
row_data["marker"]["line"]["color"], cutoff_index
|
|
434
|
+
)
|
|
435
|
+
row_data["marker"]["line"]["width"] = self.__cutoff_keep_first(
|
|
436
|
+
row_data["marker"]["line"]["width"], cutoff_index
|
|
437
|
+
)
|
|
438
|
+
row_data["hovertext"] = self.__cutoff_keep_first(row_data["hovertext"], cutoff_index)
|
|
439
|
+
row_data["hovertemplate"] = self.__cutoff_keep_first(row_data["hovertemplate"], cutoff_index)
|
|
440
|
+
|
|
441
|
+
row_data["x"][0] += removed_time
|
|
442
|
+
|
|
443
|
+
def check_row_total_time(self, row_label: str):
|
|
444
|
+
row_total_time = sum(self._completed_data_cache[row_label]["x"])
|
|
445
|
+
now = datetime.datetime.now()
|
|
446
|
+
since_start = format_timediff(self._start_time, now)
|
|
447
|
+
delta = since_start - row_total_time
|
|
448
|
+
|
|
449
|
+
if delta > 0.1:
|
|
450
|
+
# hack. need to reproduce drifting behavior and find the cause.
|
|
451
|
+
logging.info(f"Adjusting row {row_label} by {delta:.2f}s to match total runtime")
|
|
452
|
+
self._completed_data_cache[row_label]["x"][0] += delta
|
|
453
|
+
|
|
454
|
+
def __handle_task_result(self, state: StateTask, now: datetime.datetime):
|
|
455
|
+
worker = self._task_id_to_worker.get(state.task_id, "")
|
|
456
|
+
if worker == "":
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
start = None
|
|
460
|
+
task_map = self._current_tasks.get(worker)
|
|
461
|
+
if task_map:
|
|
462
|
+
start = task_map.get(state.task_id)
|
|
463
|
+
|
|
464
|
+
if start is None:
|
|
465
|
+
# we don't know when this task started, so just ignore
|
|
466
|
+
return
|
|
467
|
+
|
|
468
|
+
task_state = state.state
|
|
469
|
+
task_time = format_timediff(start, now)
|
|
470
|
+
|
|
471
|
+
row_label = self.__add_task_to_chart(worker, state.task_id, task_state, task_time)
|
|
472
|
+
self.check_row_total_time(row_label)
|
|
473
|
+
with self._data_update_lock:
|
|
474
|
+
self.__remove_task_from_worker(worker=worker, task_id=state.task_id, now=now, force_new_time=False)
|
|
475
|
+
self._row_last_used[row_label] = now
|
|
476
|
+
|
|
477
|
+
if worker_tasks := self._worker_to_task_ids.get(worker):
|
|
478
|
+
worker_tasks.discard(state.task_id)
|
|
479
|
+
|
|
480
|
+
def __handle_new_worker(self, worker: str, now: datetime.datetime, capabilities: Set[str]):
|
|
481
|
+
if worker in self._seen_workers:
|
|
482
|
+
# If for some reason we receive a task for the worker before
|
|
483
|
+
# the StateWorker connect message, just update capabilities.
|
|
484
|
+
logging.debug(f"Handling new worker {worker} but worker is already known.")
|
|
485
|
+
|
|
486
|
+
if self._worker_capabilities.get(worker) is None and capabilities is not None:
|
|
487
|
+
self._worker_capabilities[worker] = capabilities
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
row_label = f"{format_worker_name(worker)} [1]"
|
|
491
|
+
if row_label not in self._completed_data_cache:
|
|
492
|
+
self.__setup_row_cache(row_label)
|
|
493
|
+
self.__make_initial_row(worker)
|
|
494
|
+
self.__add_bar(
|
|
495
|
+
worker=worker,
|
|
496
|
+
time_taken=format_timediff(self._start_time, now),
|
|
497
|
+
task_color=TASK_STREAM_BACKGROUND_COLOR,
|
|
498
|
+
state=None,
|
|
499
|
+
hovertext="",
|
|
500
|
+
capabilities_display_string="",
|
|
501
|
+
row_label=row_label,
|
|
502
|
+
)
|
|
503
|
+
self._row_last_used[row_label] = now
|
|
504
|
+
self._seen_workers.add(worker)
|
|
505
|
+
self._worker_capabilities[worker] = capabilities
|
|
506
|
+
|
|
507
|
+
def __remove_task_from_worker(self, worker: str, task_id: bytes, now: datetime.datetime, force_new_time: bool):
|
|
508
|
+
# Remove a single task from the worker's current task mapping.
|
|
509
|
+
self.__free_row_for_task(task_id)
|
|
510
|
+
self._worker_to_task_ids.get(worker, set()).discard(task_id)
|
|
511
|
+
task_map = self._current_tasks.get(worker)
|
|
512
|
+
if not task_map:
|
|
513
|
+
return
|
|
514
|
+
task_map.pop(task_id, None)
|
|
515
|
+
if not task_map:
|
|
516
|
+
# no more tasks for this worker, remove the worker entry
|
|
517
|
+
self._current_tasks.pop(worker, None)
|
|
518
|
+
|
|
519
|
+
def __pad_inactive_time(self, worker: str, row_label: str, now: datetime.datetime):
|
|
520
|
+
"""If a row is re-used, it needs to be padded until current time"""
|
|
521
|
+
last_update = self._row_last_used.get(row_label)
|
|
522
|
+
self._row_last_used[row_label] = now
|
|
523
|
+
if not last_update:
|
|
524
|
+
return
|
|
525
|
+
idle_time = format_timediff(last_update, now)
|
|
526
|
+
self.__add_bar(
|
|
527
|
+
worker=worker,
|
|
528
|
+
time_taken=idle_time,
|
|
529
|
+
task_color=TASK_STREAM_BACKGROUND_COLOR,
|
|
530
|
+
state=None,
|
|
531
|
+
hovertext="",
|
|
532
|
+
capabilities_display_string="",
|
|
533
|
+
row_label=row_label,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def __handle_running_task(self, state_task: StateTask, worker: str, now: datetime.datetime):
|
|
537
|
+
if state_task.task_id not in self._task_id_to_printable_capabilities:
|
|
538
|
+
self._task_id_to_printable_capabilities[state_task.task_id] = display_capabilities(
|
|
539
|
+
set(state_task.capabilities.keys())
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# if another worker was previously assigned this task, remove it
|
|
543
|
+
previous_worker = self._task_id_to_worker.get(state_task.task_id)
|
|
544
|
+
if previous_worker and previous_worker != worker:
|
|
545
|
+
task_start_time = self._current_tasks.get(previous_worker, {}).get(state_task.task_id)
|
|
546
|
+
if task_start_time:
|
|
547
|
+
duration = format_timediff(task_start_time, now)
|
|
548
|
+
self.__add_task_to_chart(previous_worker, state_task.task_id, TaskState.Canceled, duration)
|
|
549
|
+
self.__remove_task_from_worker(
|
|
550
|
+
worker=previous_worker, task_id=state_task.task_id, now=now, force_new_time=False
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
self._task_id_to_worker[state_task.task_id] = worker
|
|
554
|
+
self._worker_to_object_name[worker] = state_task.function_name.decode()
|
|
555
|
+
|
|
556
|
+
worker_tasks = self._worker_to_task_ids.get(worker, set())
|
|
557
|
+
worker_tasks.add(state_task.task_id)
|
|
558
|
+
self._worker_to_task_ids[worker] = worker_tasks
|
|
559
|
+
|
|
560
|
+
# Per-task start times: store start time for this task only.
|
|
561
|
+
task_map = self._current_tasks.get(worker)
|
|
562
|
+
if task_map:
|
|
563
|
+
with self._data_update_lock:
|
|
564
|
+
task_map[state_task.task_id] = now
|
|
565
|
+
# allocate a row for this additional task so it shows on its own row
|
|
566
|
+
row_label = self.__allocate_row_for_task(worker, state_task.task_id)
|
|
567
|
+
self.__pad_inactive_time(worker, row_label, now)
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
# first task for this worker
|
|
571
|
+
with self._data_update_lock:
|
|
572
|
+
self._current_tasks[worker] = {state_task.task_id: now}
|
|
573
|
+
# allocate a row for the running task
|
|
574
|
+
self.__allocate_row_for_task(worker, state_task.task_id)
|
|
575
|
+
|
|
576
|
+
def handle_worker_state(self, state_worker: StateWorker):
|
|
577
|
+
worker_id = state_worker.worker_id.decode()
|
|
578
|
+
worker_state = state_worker.state
|
|
579
|
+
|
|
580
|
+
now = datetime.datetime.now()
|
|
581
|
+
|
|
582
|
+
if worker_state == WorkerState.Connected:
|
|
583
|
+
self.__handle_new_worker(worker_id, now, set(state_worker.capabilities.keys()))
|
|
584
|
+
elif worker_state == WorkerState.Disconnected:
|
|
585
|
+
self.mark_dead_worker(worker_id)
|
|
586
|
+
|
|
587
|
+
def handle_task_state(self, state_task: StateTask):
|
|
588
|
+
"""
|
|
589
|
+
The scheduler sends out `state.worker` while a Task is running.
|
|
590
|
+
However, as soon as the task is done, that entry is cleared.
|
|
591
|
+
A Success status will thus come with an empty `state.worker`, so
|
|
592
|
+
we store this mapping ourselves based on the Running statuses we see.
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
task_state = state_task.state
|
|
596
|
+
now = datetime.datetime.now()
|
|
597
|
+
|
|
598
|
+
if task_state in COMPLETED_TASK_STATUSES:
|
|
599
|
+
self.__handle_task_result(state_task, now)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
if not (worker := state_task.worker):
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
worker_string = worker.decode()
|
|
606
|
+
|
|
607
|
+
if worker_string not in self._seen_workers:
|
|
608
|
+
logging.warning(
|
|
609
|
+
f"Unknown worker seen in handle_task_state: {worker_string}. "
|
|
610
|
+
"Did this worker connect before the UI started?"
|
|
611
|
+
)
|
|
612
|
+
self.__handle_new_worker(worker_string, now, set())
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
if task_state in {TaskState.Running}:
|
|
616
|
+
self.__handle_running_task(state_task, worker_string, now)
|
|
617
|
+
|
|
618
|
+
def __clear_all_task_data(self, task_id: bytes):
|
|
619
|
+
self._task_id_to_worker.pop(task_id, None)
|
|
620
|
+
self._task_id_to_printable_capabilities.pop(task_id, None)
|
|
621
|
+
|
|
622
|
+
def __clear_all_worker_data(self, worker: str):
|
|
623
|
+
self._worker_to_object_name.pop(worker, None)
|
|
624
|
+
self._worker_to_task_ids.pop(worker, None)
|
|
625
|
+
self._worker_capabilities.pop(worker, None)
|
|
626
|
+
self.__clear_worker_rows(worker)
|
|
627
|
+
|
|
628
|
+
def __remove_worker_from_history(self, worker: str):
|
|
629
|
+
logging.info(f"Removing worker {worker} from task stream history")
|
|
630
|
+
for row_label in self.__clear_worker_rows(worker):
|
|
631
|
+
if row_label in self._completed_data_cache:
|
|
632
|
+
self._completed_data_cache.pop(row_label)
|
|
633
|
+
|
|
634
|
+
worker_tasks = self._worker_to_task_ids.pop(worker, set())
|
|
635
|
+
for task_id in worker_tasks:
|
|
636
|
+
self.__clear_all_task_data(task_id)
|
|
637
|
+
self.__free_row_for_task(task_id)
|
|
638
|
+
|
|
639
|
+
self.__clear_all_worker_data(worker)
|
|
640
|
+
self._seen_workers.discard(worker)
|
|
641
|
+
|
|
642
|
+
def __remove_old_tasks_from_history(self, store_duration: datetime.timedelta):
|
|
643
|
+
for row_label in self._completed_data_cache.keys():
|
|
644
|
+
row_data = self._completed_data_cache[row_label]
|
|
645
|
+
|
|
646
|
+
storage_cutoff_index = len(row_data["x"]) - 1
|
|
647
|
+
time_taken = 0
|
|
648
|
+
store_seconds = store_duration.total_seconds()
|
|
649
|
+
while storage_cutoff_index > 0 and time_taken < store_seconds:
|
|
650
|
+
time_taken += row_data["x"][storage_cutoff_index]
|
|
651
|
+
storage_cutoff_index -= 1
|
|
652
|
+
if storage_cutoff_index > 0:
|
|
653
|
+
self.__remove_old_tasks_from_cache(row_label, storage_cutoff_index)
|
|
654
|
+
|
|
655
|
+
def __remove_dead_workers(self, remove_up_to: datetime.datetime):
|
|
656
|
+
while self._dead_workers and self._dead_workers[0][0] < remove_up_to:
|
|
657
|
+
_, worker = self._dead_workers.popleft()
|
|
658
|
+
self.__remove_worker_from_history(worker)
|
|
659
|
+
|
|
660
|
+
def __split_workers_by_status(self, now: datetime.datetime) -> List[Tuple[str, float, bytes, str]]:
|
|
661
|
+
workers_doing_jobs: List[Tuple[str, float, bytes, str]] = []
|
|
662
|
+
for worker, task_map in self._current_tasks.items():
|
|
663
|
+
if not task_map:
|
|
664
|
+
continue
|
|
665
|
+
object_name = self._worker_to_object_name.get(worker, "")
|
|
666
|
+
|
|
667
|
+
# For each concurrent task, allocate/lookup a row and produce a separate entry.
|
|
668
|
+
for task_id, start_time in list(task_map.items()):
|
|
669
|
+
# determine duration relative to the task's start time
|
|
670
|
+
duration = format_timediff(start_time, now)
|
|
671
|
+
row_label = self._task_row_assignment.get(task_id)
|
|
672
|
+
if row_label is None:
|
|
673
|
+
row_label = self.__allocate_row_for_task(worker, task_id)
|
|
674
|
+
logging.warning(f"split worker needed to allocate row {row_label} for task {task_id.hex()}")
|
|
675
|
+
|
|
676
|
+
workers_doing_jobs.append((row_label, duration, task_id, object_name))
|
|
677
|
+
return workers_doing_jobs
|
|
678
|
+
|
|
679
|
+
def mark_dead_worker(self, worker_name: str):
|
|
680
|
+
now = datetime.datetime.now()
|
|
681
|
+
with self._data_update_lock:
|
|
682
|
+
self._current_tasks.pop(worker_name, None)
|
|
683
|
+
self._dead_workers.append((now, worker_name))
|
|
684
|
+
|
|
685
|
+
def update_plot(self):
|
|
686
|
+
with self._data_update_lock:
|
|
687
|
+
now = datetime.datetime.now()
|
|
688
|
+
|
|
689
|
+
workers_doing_tasks = self.__split_workers_by_status(now)
|
|
690
|
+
|
|
691
|
+
self.__remove_old_tasks_from_history(self._settings.memory_store_time)
|
|
692
|
+
|
|
693
|
+
worker_retention_time = now - self._settings.memory_store_time
|
|
694
|
+
self.__remove_dead_workers(worker_retention_time)
|
|
695
|
+
|
|
696
|
+
worker_display_time = now - self._settings.stream_window
|
|
697
|
+
hidden_rows = {
|
|
698
|
+
row
|
|
699
|
+
for row in self._row_last_used.keys()
|
|
700
|
+
if self._row_last_used[row] < worker_display_time and self._row_to_worker[row] in self._dead_workers
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
completed_cache_values = sorted(
|
|
704
|
+
list(x for x in self._completed_data_cache.values() if x["y"] and x["y"][0] not in hidden_rows),
|
|
705
|
+
key=lambda d: d["y"][0],
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
task_ids = [t for (_, _, t, _) in workers_doing_tasks]
|
|
709
|
+
task_capabilities = [
|
|
710
|
+
(
|
|
711
|
+
f"Capabilities: {self._task_id_to_printable_capabilities.get(task_id, '')}"
|
|
712
|
+
if task_id in self._task_id_to_printable_capabilities
|
|
713
|
+
else ""
|
|
714
|
+
)
|
|
715
|
+
for task_id in task_ids
|
|
716
|
+
]
|
|
717
|
+
task_colors = [self.__get_task_color(t) for t in task_ids]
|
|
718
|
+
running_shape = TaskShapes.from_status(TaskState.Running)
|
|
719
|
+
working_data = {
|
|
720
|
+
"type": "bar",
|
|
721
|
+
"name": "Working",
|
|
722
|
+
"y": [w for (w, _, _, _) in workers_doing_tasks],
|
|
723
|
+
"x": [t for (_, t, _, _) in workers_doing_tasks],
|
|
724
|
+
"orientation": "h",
|
|
725
|
+
"text": [f for (_, _, _, f) in workers_doing_tasks],
|
|
726
|
+
"hovertemplate": "%{text} (%{x})<br>%{customdata}",
|
|
727
|
+
"marker": {
|
|
728
|
+
"color": task_colors,
|
|
729
|
+
"width": 5,
|
|
730
|
+
"pattern": {"shape": [running_shape for _ in workers_doing_tasks]},
|
|
731
|
+
"line": {"color": ["yellow" for _ in workers_doing_tasks], "width": [1 for _ in workers_doing_tasks]},
|
|
732
|
+
},
|
|
733
|
+
"textfont": {"color": "black", "outline": "white", "outlinewidth": 5},
|
|
734
|
+
"customdata": task_capabilities,
|
|
735
|
+
"showlegend": False,
|
|
736
|
+
}
|
|
737
|
+
plot_data = completed_cache_values + [working_data]
|
|
738
|
+
self._figure["data"] = plot_data
|
|
739
|
+
self.__render_plot(now)
|
|
740
|
+
|
|
741
|
+
def __build_capability_boxes(self, labels: List[str], lower_bound: int, upper_bound: int) -> Tuple[Dict, Dict]:
|
|
742
|
+
"""
|
|
743
|
+
Creates a legend-style set of colored boxes indicating worker capabilities.
|
|
744
|
+
Returns two traces: one for the colored boxes, and one for white background boxes to hide bars underneath.
|
|
745
|
+
"""
|
|
746
|
+
window = max(1, upper_bound - lower_bound)
|
|
747
|
+
base_x = lower_bound + window * 0.01
|
|
748
|
+
offset = window * 0.01
|
|
749
|
+
|
|
750
|
+
worker_data: Dict[str, Dict] = {}
|
|
751
|
+
for worker, capabilities in self._worker_capabilities.items():
|
|
752
|
+
worker_data[worker] = {"x_vals": [], "colors": [], "customdata": []}
|
|
753
|
+
# capability_count = len(capabilities)
|
|
754
|
+
for index, capability in enumerate(sorted(capabilities)):
|
|
755
|
+
color = self.__get_capabilities_color(capability)
|
|
756
|
+
x = base_x + index * offset
|
|
757
|
+
worker_data[worker]["x_vals"].append(x)
|
|
758
|
+
worker_data[worker]["colors"].append(color)
|
|
759
|
+
worker_data[worker]["customdata"].append(capability)
|
|
760
|
+
|
|
761
|
+
x_vals = []
|
|
762
|
+
y_vals = []
|
|
763
|
+
colors = []
|
|
764
|
+
customdata = []
|
|
765
|
+
|
|
766
|
+
for row in labels:
|
|
767
|
+
worker = self._row_to_worker.get(row)
|
|
768
|
+
box_data = worker_data.get(worker)
|
|
769
|
+
if not box_data:
|
|
770
|
+
continue
|
|
771
|
+
|
|
772
|
+
x_vals.extend(box_data["x_vals"])
|
|
773
|
+
y_vals.extend([row] * len(box_data["x_vals"]))
|
|
774
|
+
colors.extend(box_data["colors"])
|
|
775
|
+
customdata.extend(box_data["customdata"])
|
|
776
|
+
|
|
777
|
+
# Background trace: slightly larger white squares to hide underlying bars where capability boxes sit
|
|
778
|
+
background_trace = {
|
|
779
|
+
"type": "scatter",
|
|
780
|
+
"mode": "markers",
|
|
781
|
+
"name": "",
|
|
782
|
+
"x": x_vals,
|
|
783
|
+
"y": y_vals,
|
|
784
|
+
"marker": {"symbol": "square", "size": 12, "color": "white", "line": {"color": "white", "width": 1}},
|
|
785
|
+
"hoverinfo": "skip",
|
|
786
|
+
"showlegend": False,
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
trace = {
|
|
790
|
+
"type": "scatter",
|
|
791
|
+
"mode": "markers",
|
|
792
|
+
"name": "Capabilities",
|
|
793
|
+
"x": x_vals,
|
|
794
|
+
"y": y_vals,
|
|
795
|
+
"marker": {"symbol": "square", "size": 10, "color": colors, "line": {"color": "black", "width": 1}},
|
|
796
|
+
"customdata": customdata,
|
|
797
|
+
"hovertemplate": "%{customdata}",
|
|
798
|
+
"showlegend": False,
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return trace, background_trace
|
|
802
|
+
|
|
803
|
+
def __adjust_card_height(self, num_rows: int):
|
|
804
|
+
# these are estimates, may need adjustments for large row counts
|
|
805
|
+
min_height = 800
|
|
806
|
+
row_height = 20
|
|
807
|
+
header_height = 140
|
|
808
|
+
estimated_height = header_height + (num_rows * row_height)
|
|
809
|
+
target_height = max(min_height, estimated_height)
|
|
810
|
+
self._card.style(f"height: {target_height}px; overflow:auto;")
|
|
811
|
+
|
|
812
|
+
def __render_plot(self, now: datetime.datetime):
|
|
813
|
+
lower_bound, upper_bound = get_bounds(now, self._start_time, self._settings)
|
|
814
|
+
|
|
815
|
+
ticks = make_taskstream_ticks(lower_bound, upper_bound)
|
|
816
|
+
tick_text = make_tick_text(int(self._settings.stream_window.total_seconds()))
|
|
817
|
+
|
|
818
|
+
if self._user_axis_range:
|
|
819
|
+
self._figure["layout"]["xaxis"]["range"] = self._user_axis_range
|
|
820
|
+
else:
|
|
821
|
+
self._figure["layout"]["xaxis"]["range"] = [lower_bound, upper_bound]
|
|
822
|
+
self._figure["layout"]["xaxis"]["tickvals"] = ticks
|
|
823
|
+
self._figure["layout"]["xaxis"]["ticktext"] = tick_text
|
|
824
|
+
|
|
825
|
+
seen = set()
|
|
826
|
+
category_array: List[str] = []
|
|
827
|
+
for trace in self._figure.get("data", []):
|
|
828
|
+
if trace.get("orientation") == "h" and trace.get("y"):
|
|
829
|
+
for yval in trace.get("y", []):
|
|
830
|
+
if yval not in seen:
|
|
831
|
+
seen.add(yval)
|
|
832
|
+
category_array.append(yval)
|
|
833
|
+
|
|
834
|
+
capability_traces, background_trace = self.__build_capability_boxes(category_array, lower_bound, upper_bound)
|
|
835
|
+
|
|
836
|
+
self._figure["data"].append(background_trace)
|
|
837
|
+
self._figure["data"].append(capability_traces)
|
|
838
|
+
|
|
839
|
+
self.__adjust_card_height(len(category_array))
|
|
840
|
+
self._plot.update()
|