toil 6.1.0a1__py3-none-any.whl → 8.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.
- toil/__init__.py +122 -315
- toil/batchSystems/__init__.py +1 -0
- toil/batchSystems/abstractBatchSystem.py +173 -89
- toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
- toil/batchSystems/awsBatch.py +244 -135
- toil/batchSystems/cleanup_support.py +26 -16
- toil/batchSystems/contained_executor.py +31 -28
- toil/batchSystems/gridengine.py +86 -50
- toil/batchSystems/htcondor.py +166 -89
- toil/batchSystems/kubernetes.py +632 -382
- toil/batchSystems/local_support.py +20 -15
- toil/batchSystems/lsf.py +134 -81
- toil/batchSystems/lsfHelper.py +13 -11
- toil/batchSystems/mesos/__init__.py +41 -29
- toil/batchSystems/mesos/batchSystem.py +290 -151
- toil/batchSystems/mesos/executor.py +79 -50
- toil/batchSystems/mesos/test/__init__.py +31 -23
- toil/batchSystems/options.py +46 -28
- toil/batchSystems/registry.py +53 -19
- toil/batchSystems/singleMachine.py +296 -125
- toil/batchSystems/slurm.py +603 -138
- toil/batchSystems/torque.py +47 -33
- toil/bus.py +186 -76
- toil/common.py +664 -368
- toil/cwl/__init__.py +1 -1
- toil/cwl/cwltoil.py +1136 -483
- toil/cwl/utils.py +17 -22
- toil/deferred.py +63 -42
- toil/exceptions.py +5 -3
- toil/fileStores/__init__.py +5 -5
- toil/fileStores/abstractFileStore.py +140 -60
- toil/fileStores/cachingFileStore.py +717 -269
- toil/fileStores/nonCachingFileStore.py +116 -87
- toil/job.py +1225 -368
- toil/jobStores/abstractJobStore.py +416 -266
- toil/jobStores/aws/jobStore.py +863 -477
- toil/jobStores/aws/utils.py +201 -120
- toil/jobStores/conftest.py +3 -2
- toil/jobStores/fileJobStore.py +292 -154
- toil/jobStores/googleJobStore.py +140 -74
- toil/jobStores/utils.py +36 -15
- toil/leader.py +668 -272
- toil/lib/accelerators.py +115 -18
- toil/lib/aws/__init__.py +74 -31
- toil/lib/aws/ami.py +122 -87
- toil/lib/aws/iam.py +284 -108
- toil/lib/aws/s3.py +31 -0
- toil/lib/aws/session.py +214 -39
- toil/lib/aws/utils.py +287 -231
- toil/lib/bioio.py +13 -5
- toil/lib/compatibility.py +11 -6
- toil/lib/conversions.py +104 -47
- toil/lib/docker.py +131 -103
- toil/lib/ec2.py +361 -199
- toil/lib/ec2nodes.py +174 -106
- toil/lib/encryption/_dummy.py +5 -3
- toil/lib/encryption/_nacl.py +10 -6
- toil/lib/encryption/conftest.py +1 -0
- toil/lib/exceptions.py +26 -7
- toil/lib/expando.py +5 -3
- toil/lib/ftp_utils.py +217 -0
- toil/lib/generatedEC2Lists.py +127 -19
- toil/lib/humanize.py +6 -2
- toil/lib/integration.py +341 -0
- toil/lib/io.py +141 -15
- toil/lib/iterables.py +4 -2
- toil/lib/memoize.py +12 -8
- toil/lib/misc.py +66 -21
- toil/lib/objects.py +2 -2
- toil/lib/resources.py +68 -15
- toil/lib/retry.py +126 -81
- toil/lib/threading.py +299 -82
- toil/lib/throttle.py +16 -15
- toil/options/common.py +843 -409
- toil/options/cwl.py +175 -90
- toil/options/runner.py +50 -0
- toil/options/wdl.py +73 -17
- toil/provisioners/__init__.py +117 -46
- toil/provisioners/abstractProvisioner.py +332 -157
- toil/provisioners/aws/__init__.py +70 -33
- toil/provisioners/aws/awsProvisioner.py +1145 -715
- toil/provisioners/clusterScaler.py +541 -279
- toil/provisioners/gceProvisioner.py +282 -179
- toil/provisioners/node.py +155 -79
- toil/realtimeLogger.py +34 -22
- toil/resource.py +137 -75
- toil/server/app.py +128 -62
- toil/server/celery_app.py +3 -1
- toil/server/cli/wes_cwl_runner.py +82 -53
- toil/server/utils.py +54 -28
- toil/server/wes/abstract_backend.py +64 -26
- toil/server/wes/amazon_wes_utils.py +21 -15
- toil/server/wes/tasks.py +121 -63
- toil/server/wes/toil_backend.py +142 -107
- toil/server/wsgi_app.py +4 -3
- toil/serviceManager.py +58 -22
- toil/statsAndLogging.py +224 -70
- toil/test/__init__.py +282 -183
- toil/test/batchSystems/batchSystemTest.py +460 -210
- toil/test/batchSystems/batch_system_plugin_test.py +90 -0
- toil/test/batchSystems/test_gridengine.py +173 -0
- toil/test/batchSystems/test_lsf_helper.py +67 -58
- toil/test/batchSystems/test_slurm.py +110 -49
- toil/test/cactus/__init__.py +0 -0
- toil/test/cactus/test_cactus_integration.py +56 -0
- toil/test/cwl/cwlTest.py +496 -287
- toil/test/cwl/measure_default_memory.cwl +12 -0
- toil/test/cwl/not_run_required_input.cwl +29 -0
- toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
- toil/test/cwl/seqtk_seq.cwl +1 -1
- toil/test/docs/scriptsTest.py +69 -46
- toil/test/jobStores/jobStoreTest.py +427 -264
- toil/test/lib/aws/test_iam.py +118 -50
- toil/test/lib/aws/test_s3.py +16 -9
- toil/test/lib/aws/test_utils.py +5 -6
- toil/test/lib/dockerTest.py +118 -141
- toil/test/lib/test_conversions.py +113 -115
- toil/test/lib/test_ec2.py +58 -50
- toil/test/lib/test_integration.py +104 -0
- toil/test/lib/test_misc.py +12 -5
- toil/test/mesos/MesosDataStructuresTest.py +23 -10
- toil/test/mesos/helloWorld.py +7 -6
- toil/test/mesos/stress.py +25 -20
- toil/test/options/__init__.py +13 -0
- toil/test/options/options.py +42 -0
- toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
- toil/test/provisioners/clusterScalerTest.py +440 -250
- toil/test/provisioners/clusterTest.py +166 -44
- toil/test/provisioners/gceProvisionerTest.py +174 -100
- toil/test/provisioners/provisionerTest.py +25 -13
- toil/test/provisioners/restartScript.py +5 -4
- toil/test/server/serverTest.py +188 -141
- toil/test/sort/restart_sort.py +137 -68
- toil/test/sort/sort.py +134 -66
- toil/test/sort/sortTest.py +91 -49
- toil/test/src/autoDeploymentTest.py +141 -101
- toil/test/src/busTest.py +20 -18
- toil/test/src/checkpointTest.py +8 -2
- toil/test/src/deferredFunctionTest.py +49 -35
- toil/test/src/dockerCheckTest.py +32 -24
- toil/test/src/environmentTest.py +135 -0
- toil/test/src/fileStoreTest.py +539 -272
- toil/test/src/helloWorldTest.py +7 -4
- toil/test/src/importExportFileTest.py +61 -31
- toil/test/src/jobDescriptionTest.py +46 -21
- toil/test/src/jobEncapsulationTest.py +2 -0
- toil/test/src/jobFileStoreTest.py +74 -50
- toil/test/src/jobServiceTest.py +187 -73
- toil/test/src/jobTest.py +121 -71
- toil/test/src/miscTests.py +19 -18
- toil/test/src/promisedRequirementTest.py +82 -36
- toil/test/src/promisesTest.py +7 -6
- toil/test/src/realtimeLoggerTest.py +10 -6
- toil/test/src/regularLogTest.py +71 -37
- toil/test/src/resourceTest.py +80 -49
- toil/test/src/restartDAGTest.py +36 -22
- toil/test/src/resumabilityTest.py +9 -2
- toil/test/src/retainTempDirTest.py +45 -14
- toil/test/src/systemTest.py +12 -8
- toil/test/src/threadingTest.py +44 -25
- toil/test/src/toilContextManagerTest.py +10 -7
- toil/test/src/userDefinedJobArgTypeTest.py +8 -5
- toil/test/src/workerTest.py +73 -23
- toil/test/utils/toilDebugTest.py +103 -33
- toil/test/utils/toilKillTest.py +4 -5
- toil/test/utils/utilsTest.py +245 -106
- toil/test/wdl/wdltoil_test.py +818 -149
- toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
- toil/toilState.py +120 -35
- toil/utils/toilConfig.py +13 -4
- toil/utils/toilDebugFile.py +44 -27
- toil/utils/toilDebugJob.py +214 -27
- toil/utils/toilDestroyCluster.py +11 -6
- toil/utils/toilKill.py +8 -3
- toil/utils/toilLaunchCluster.py +256 -140
- toil/utils/toilMain.py +37 -16
- toil/utils/toilRsyncCluster.py +32 -14
- toil/utils/toilSshCluster.py +49 -22
- toil/utils/toilStats.py +356 -273
- toil/utils/toilStatus.py +292 -139
- toil/utils/toilUpdateEC2Instances.py +3 -1
- toil/version.py +12 -12
- toil/wdl/utils.py +5 -5
- toil/wdl/wdltoil.py +3913 -1033
- toil/worker.py +367 -184
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
- toil-8.0.0.dist-info/METADATA +173 -0
- toil-8.0.0.dist-info/RECORD +253 -0
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
- toil-6.1.0a1.dist-info/METADATA +0 -125
- toil-6.1.0a1.dist-info/RECORD +0 -237
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
toil/utils/toilStats.py
CHANGED
|
@@ -14,18 +14,57 @@
|
|
|
14
14
|
"""Reports statistical data about a given Toil workflow."""
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
|
+
import math
|
|
18
|
+
import sys
|
|
17
19
|
from argparse import ArgumentParser, Namespace
|
|
18
20
|
from functools import partial
|
|
19
|
-
from typing import Any, Callable,
|
|
21
|
+
from typing import Any, Callable, Optional, TextIO, Union
|
|
20
22
|
|
|
21
23
|
from toil.common import Config, Toil, parser_with_common_options
|
|
22
24
|
from toil.job import Job
|
|
23
|
-
from toil.jobStores.abstractJobStore import AbstractJobStore
|
|
25
|
+
from toil.jobStores.abstractJobStore import AbstractJobStore, NoSuchJobStoreException
|
|
24
26
|
from toil.lib.expando import Expando
|
|
27
|
+
from toil.options.common import SYS_MAX_SIZE
|
|
25
28
|
from toil.statsAndLogging import set_logging_from_options
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
28
31
|
|
|
32
|
+
# These categories of stat will be reported
|
|
33
|
+
CATEGORIES = ["time", "clock", "wait", "memory", "disk"]
|
|
34
|
+
# These are the units they are stored in
|
|
35
|
+
CATEGORY_UNITS = {
|
|
36
|
+
"time": "s",
|
|
37
|
+
"clock": "core-s",
|
|
38
|
+
"wait": "core-s",
|
|
39
|
+
"memory": "KiB",
|
|
40
|
+
"disk": "B",
|
|
41
|
+
}
|
|
42
|
+
# These are what we call them to the user
|
|
43
|
+
TITLES = {
|
|
44
|
+
"time": "Real Time",
|
|
45
|
+
"clock": "CPU Time",
|
|
46
|
+
"wait": "CPU Wait",
|
|
47
|
+
"memory": "Memory",
|
|
48
|
+
"disk": "Disk",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Of those, these are in time
|
|
52
|
+
TIME_CATEGORIES = {"time", "clock", "wait"}
|
|
53
|
+
# And these are in space
|
|
54
|
+
SPACE_CATEGORIES = {"memory", "disk"}
|
|
55
|
+
# These categories aren't stored and need to be computed
|
|
56
|
+
COMPUTED_CATEGORIES = {"wait"}
|
|
57
|
+
|
|
58
|
+
# The different kinds of summaries have both short and long names, and we need
|
|
59
|
+
# to convert between them.
|
|
60
|
+
LONG_FORMS = {
|
|
61
|
+
"med": "median",
|
|
62
|
+
"ave": "average",
|
|
63
|
+
"min": "min",
|
|
64
|
+
"total": "total",
|
|
65
|
+
"max": "max",
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
|
|
30
69
|
class ColumnWidths:
|
|
31
70
|
"""
|
|
@@ -33,33 +72,33 @@ class ColumnWidths:
|
|
|
33
72
|
"""
|
|
34
73
|
|
|
35
74
|
def __init__(self) -> None:
|
|
36
|
-
self.categories =
|
|
75
|
+
self.categories = CATEGORIES
|
|
37
76
|
self.fields_count = ["count", "min", "med", "ave", "max", "total"]
|
|
38
77
|
self.fields = ["min", "med", "ave", "max", "total"]
|
|
39
|
-
self.data:
|
|
78
|
+
self.data: dict[str, int] = {}
|
|
40
79
|
for category in self.categories:
|
|
41
80
|
for field in self.fields_count:
|
|
42
|
-
self.
|
|
81
|
+
self.set_width(category, field, 8)
|
|
43
82
|
|
|
44
83
|
def title(self, category: str) -> int:
|
|
45
84
|
"""Return the total printed length of this category item."""
|
|
46
|
-
return sum(self.
|
|
85
|
+
return sum(self.get_width(category, x) for x in self.fields)
|
|
47
86
|
|
|
48
|
-
def
|
|
87
|
+
def get_width(self, category: str, field: str) -> int:
|
|
49
88
|
category = category.lower()
|
|
50
89
|
return self.data[f"{category}_{field}"]
|
|
51
90
|
|
|
52
|
-
def
|
|
91
|
+
def set_width(self, category: str, field: str, width: int) -> None:
|
|
53
92
|
category = category.lower()
|
|
54
93
|
self.data[f"{category}_{field}"] = width
|
|
55
94
|
|
|
56
95
|
def report(self) -> None:
|
|
57
96
|
for c in self.categories:
|
|
58
97
|
for f in self.fields:
|
|
59
|
-
print("%s %s %d" % (c, f, self.
|
|
98
|
+
print("%s %s %d" % (c, f, self.get_width(c, f)))
|
|
60
99
|
|
|
61
100
|
|
|
62
|
-
def
|
|
101
|
+
def pad_str(s: str, field: Optional[int] = None) -> str:
|
|
63
102
|
"""Pad the beginning of a string with spaces, if necessary."""
|
|
64
103
|
if field is None or len(s) >= field:
|
|
65
104
|
return s
|
|
@@ -67,48 +106,60 @@ def padStr(s: str, field: Optional[int] = None) -> str:
|
|
|
67
106
|
return " " * (field - len(s)) + s
|
|
68
107
|
|
|
69
108
|
|
|
70
|
-
def
|
|
71
|
-
"""Given input k as
|
|
72
|
-
|
|
73
|
-
|
|
109
|
+
def pretty_space(k: float, field: Optional[int] = None, alone: bool = False) -> str:
|
|
110
|
+
"""Given input k as kibibytes, return a nicely formatted string."""
|
|
111
|
+
# If we don't have a header to say bytes, include the B.
|
|
112
|
+
trailer = "B" if alone else ""
|
|
74
113
|
if k < 1024:
|
|
75
|
-
return
|
|
114
|
+
return pad_str("{:g}Ki{}".format(k, trailer), field)
|
|
76
115
|
if k < (1024 * 1024):
|
|
77
|
-
return
|
|
116
|
+
return pad_str("{:.1f}Mi{}".format(k / 1024.0, trailer), field)
|
|
78
117
|
if k < (1024 * 1024 * 1024):
|
|
79
|
-
return
|
|
118
|
+
return pad_str("{:.1f}Gi{}".format(k / 1024.0 / 1024.0, trailer), field)
|
|
80
119
|
if k < (1024 * 1024 * 1024 * 1024):
|
|
81
|
-
return
|
|
120
|
+
return pad_str(
|
|
121
|
+
"{:.1f}Ti{}".format(k / 1024.0 / 1024.0 / 1024.0, trailer), field
|
|
122
|
+
)
|
|
82
123
|
if k < (1024 * 1024 * 1024 * 1024 * 1024):
|
|
83
|
-
return
|
|
124
|
+
return pad_str(
|
|
125
|
+
"{:.1f}Pi{}".format(k / 1024.0 / 1024.0 / 1024.0 / 1024.0, trailer), field
|
|
126
|
+
)
|
|
84
127
|
|
|
85
128
|
# due to https://stackoverflow.com/questions/47149154
|
|
86
129
|
assert False
|
|
87
130
|
|
|
88
131
|
|
|
89
|
-
def
|
|
90
|
-
|
|
132
|
+
def pretty_time(
|
|
133
|
+
t: float, field: Optional[int] = None, unit: str = "s", alone: bool = False
|
|
134
|
+
) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Given input t as seconds, return a nicely formatted string.
|
|
137
|
+
"""
|
|
138
|
+
assert unit in ("s", "core-s")
|
|
139
|
+
# Qualify our CPU times as CPU time if we aren't in a table that does that
|
|
140
|
+
unit_str = report_unit(unit) if alone else "s"
|
|
141
|
+
|
|
91
142
|
from math import floor
|
|
92
143
|
|
|
93
144
|
pluralDict = {True: "s", False: ""}
|
|
94
145
|
if t < 120:
|
|
95
|
-
return
|
|
146
|
+
return pad_str("%d%s" % (t, unit_str), field)
|
|
96
147
|
if t < 120 * 60:
|
|
97
148
|
m = floor(t / 60.0)
|
|
98
149
|
s = t % 60
|
|
99
|
-
return
|
|
150
|
+
return pad_str("%dm%d%s" % (m, s, unit_str), field)
|
|
100
151
|
if t < 25 * 60 * 60:
|
|
101
152
|
h = floor(t / 60.0 / 60.0)
|
|
102
153
|
m = floor((t - (h * 60.0 * 60.0)) / 60.0)
|
|
103
154
|
s = t % 60
|
|
104
|
-
return
|
|
155
|
+
return pad_str("%dh%gm%d%s" % (h, m, s, unit_str), field)
|
|
105
156
|
if t < 7 * 24 * 60 * 60:
|
|
106
157
|
d = floor(t / 24.0 / 60.0 / 60.0)
|
|
107
158
|
h = floor((t - (d * 24.0 * 60.0 * 60.0)) / 60.0 / 60.0)
|
|
108
159
|
m = floor((t - (d * 24.0 * 60.0 * 60.0) - (h * 60.0 * 60.0)) / 60.0)
|
|
109
160
|
s = t % 60
|
|
110
161
|
dPlural = pluralDict[d > 1]
|
|
111
|
-
return
|
|
162
|
+
return pad_str("%dday%s%dh%dm%d%s" % (d, dPlural, h, m, s, unit_str), field)
|
|
112
163
|
w = floor(t / 7.0 / 24.0 / 60.0 / 60.0)
|
|
113
164
|
d = floor((t - (w * 7 * 24 * 60 * 60)) / 24.0 / 60.0 / 60.0)
|
|
114
165
|
h = floor(
|
|
@@ -126,39 +177,109 @@ def prettyTime(t: float, field: Optional[int] = None) -> str:
|
|
|
126
177
|
s = t % 60
|
|
127
178
|
wPlural = pluralDict[w > 1]
|
|
128
179
|
dPlural = pluralDict[d > 1]
|
|
129
|
-
return
|
|
180
|
+
return pad_str(
|
|
181
|
+
"%dweek%s%dday%s%dh%dm%d%s" % (w, wPlural, d, dPlural, h, m, s, unit_str), field
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def report_unit(unit: str) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Format a unit name for display.
|
|
188
|
+
"""
|
|
189
|
+
if unit == "core-s":
|
|
190
|
+
return "core·s"
|
|
191
|
+
return unit
|
|
130
192
|
|
|
131
193
|
|
|
132
|
-
def
|
|
194
|
+
def report_time(
|
|
195
|
+
t: float,
|
|
196
|
+
options: Namespace,
|
|
197
|
+
field: Optional[int] = None,
|
|
198
|
+
unit: str = "s",
|
|
199
|
+
alone: bool = False,
|
|
200
|
+
) -> str:
|
|
133
201
|
"""Given t seconds, report back the correct format as string."""
|
|
202
|
+
assert unit in ("s", "core-s")
|
|
134
203
|
if options.pretty:
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
204
|
+
return pretty_time(t, field=field, unit=unit, alone=alone)
|
|
205
|
+
unit_text = f" {report_unit(unit)}" if alone else ""
|
|
206
|
+
if field is not None:
|
|
207
|
+
assert field >= len(unit_text)
|
|
208
|
+
return "%*.2f%s" % (field - len(unit_text), t, unit_text)
|
|
209
|
+
return "{:.2f}{}".format(t, unit_text)
|
|
139
210
|
|
|
140
211
|
|
|
141
|
-
def
|
|
142
|
-
k: float,
|
|
212
|
+
def report_space(
|
|
213
|
+
k: float,
|
|
214
|
+
options: Namespace,
|
|
215
|
+
field: Optional[int] = None,
|
|
216
|
+
unit: str = "KiB",
|
|
217
|
+
alone: bool = False,
|
|
143
218
|
) -> str:
|
|
144
|
-
"""
|
|
219
|
+
"""
|
|
220
|
+
Given k kibibytes, report back the correct format as string.
|
|
221
|
+
|
|
222
|
+
If unit is set to B, convert to KiB first.
|
|
223
|
+
"""
|
|
224
|
+
if unit == "B":
|
|
225
|
+
k /= 1024.0
|
|
226
|
+
unit = "KiB"
|
|
227
|
+
assert unit == "KiB"
|
|
145
228
|
if options.pretty:
|
|
146
|
-
return
|
|
229
|
+
return pretty_space(int(k), field=field, alone=alone)
|
|
147
230
|
else:
|
|
148
|
-
|
|
149
|
-
|
|
231
|
+
# If we don't have a heading to say bytes, include the B
|
|
232
|
+
trailer = "KiB" if alone else "Ki"
|
|
150
233
|
if field is not None:
|
|
151
|
-
|
|
234
|
+
assert field >= len(trailer)
|
|
235
|
+
return "%*d%s" % (field - len(trailer), k, trailer)
|
|
152
236
|
else:
|
|
153
|
-
return "%
|
|
237
|
+
return "%d%s" % (int(k), trailer)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def report_number(
|
|
241
|
+
n: Union[int, float, None], field: Optional[int] = None, nan_value: str = "NaN"
|
|
242
|
+
) -> str:
|
|
243
|
+
"""
|
|
244
|
+
Given a number, report back the correct format as string.
|
|
245
|
+
|
|
246
|
+
If it is a NaN or None, use nan_value to represent it instead.
|
|
247
|
+
"""
|
|
248
|
+
if n is None or math.isnan(n):
|
|
249
|
+
return pad_str(nan_value, field=field)
|
|
250
|
+
else:
|
|
251
|
+
# Make sure not to format with too much precision for the field size;
|
|
252
|
+
# leave room for . and the spacing to the previous field.
|
|
253
|
+
return "%*.*g" % (field, field - 2, n) if field else "%g" % n
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def report(
|
|
257
|
+
v: float,
|
|
258
|
+
category: str,
|
|
259
|
+
options: Namespace,
|
|
260
|
+
field: Optional[int] = None,
|
|
261
|
+
alone=False,
|
|
262
|
+
) -> str:
|
|
263
|
+
"""
|
|
264
|
+
Report a value of the given category formatted as a string.
|
|
154
265
|
|
|
266
|
+
Uses the given field width if set.
|
|
155
267
|
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
|
|
268
|
+
If alone is set, the field is being formatted outside a table and might need a unit.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
unit = CATEGORY_UNITS.get(category)
|
|
272
|
+
if unit in ("s", "core-s"):
|
|
273
|
+
# This is time.
|
|
274
|
+
return report_time(v, options, field=field, unit=unit, alone=alone)
|
|
275
|
+
elif unit in ("B", "KiB"):
|
|
276
|
+
# This is space.
|
|
277
|
+
return report_space(v, options, field=field, unit=unit, alone=alone)
|
|
278
|
+
else:
|
|
279
|
+
raise ValueError(f"Unimplemented unit {unit} for category {category}")
|
|
159
280
|
|
|
160
281
|
|
|
161
|
-
def
|
|
282
|
+
def sprint_tag(
|
|
162
283
|
key: str,
|
|
163
284
|
tag: Expando,
|
|
164
285
|
options: Namespace,
|
|
@@ -167,9 +288,9 @@ def sprintTag(
|
|
|
167
288
|
"""Generate a pretty-print ready string from a JTTag()."""
|
|
168
289
|
if columnWidths is None:
|
|
169
290
|
columnWidths = ColumnWidths()
|
|
170
|
-
header = " %7s " %
|
|
291
|
+
header = " %7s " % decorate_title("count", "Count", options)
|
|
171
292
|
sub_header = " %7s " % "n"
|
|
172
|
-
tag_str = f" {
|
|
293
|
+
tag_str = f" {report_number(n=tag.total_number, field=7)}"
|
|
173
294
|
out_str = ""
|
|
174
295
|
if key == "job":
|
|
175
296
|
out_str += " {:<12} | {:>7}{:>7}{:>7}{:>7}\n".format(
|
|
@@ -182,109 +303,82 @@ def sprintTag(
|
|
|
182
303
|
tag.average_number_per_worker,
|
|
183
304
|
tag.max_number_per_worker,
|
|
184
305
|
]:
|
|
185
|
-
worker_str +=
|
|
306
|
+
worker_str += report_number(n=t, field=7)
|
|
186
307
|
out_str += worker_str + "\n"
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
sub_header += decorateSubHeader("Time", columnWidths, options)
|
|
193
|
-
tag_str += " | "
|
|
194
|
-
for t, width in [
|
|
195
|
-
(tag.min_time, columnWidths.getWidth("time", "min")),
|
|
196
|
-
(tag.median_time, columnWidths.getWidth("time", "med")),
|
|
197
|
-
(tag.average_time, columnWidths.getWidth("time", "ave")),
|
|
198
|
-
(tag.max_time, columnWidths.getWidth("time", "max")),
|
|
199
|
-
(tag.total_time, columnWidths.getWidth("time", "total")),
|
|
200
|
-
]:
|
|
201
|
-
tag_str += reportTime(t, options, field=width)
|
|
202
|
-
if "clock" in options.categories:
|
|
203
|
-
header += "| %*s " % (
|
|
204
|
-
columnWidths.title("clock"),
|
|
205
|
-
decorateTitle("Clock", options),
|
|
206
|
-
)
|
|
207
|
-
sub_header += decorateSubHeader("Clock", columnWidths, options)
|
|
208
|
-
tag_str += " | "
|
|
209
|
-
for t, width in [
|
|
210
|
-
(tag.min_clock, columnWidths.getWidth("clock", "min")),
|
|
211
|
-
(tag.median_clock, columnWidths.getWidth("clock", "med")),
|
|
212
|
-
(tag.average_clock, columnWidths.getWidth("clock", "ave")),
|
|
213
|
-
(tag.max_clock, columnWidths.getWidth("clock", "max")),
|
|
214
|
-
(tag.total_clock, columnWidths.getWidth("clock", "total")),
|
|
215
|
-
]:
|
|
216
|
-
tag_str += reportTime(t, options, field=width)
|
|
217
|
-
if "wait" in options.categories:
|
|
218
|
-
header += "| %*s " % (
|
|
219
|
-
columnWidths.title("wait"),
|
|
220
|
-
decorateTitle("Wait", options),
|
|
221
|
-
)
|
|
222
|
-
sub_header += decorateSubHeader("Wait", columnWidths, options)
|
|
223
|
-
tag_str += " | "
|
|
224
|
-
for t, width in [
|
|
225
|
-
(tag.min_wait, columnWidths.getWidth("wait", "min")),
|
|
226
|
-
(tag.median_wait, columnWidths.getWidth("wait", "med")),
|
|
227
|
-
(tag.average_wait, columnWidths.getWidth("wait", "ave")),
|
|
228
|
-
(tag.max_wait, columnWidths.getWidth("wait", "max")),
|
|
229
|
-
(tag.total_wait, columnWidths.getWidth("wait", "total")),
|
|
230
|
-
]:
|
|
231
|
-
tag_str += reportTime(t, options, field=width)
|
|
232
|
-
if "memory" in options.categories:
|
|
308
|
+
|
|
309
|
+
for category in CATEGORIES:
|
|
310
|
+
if category not in options.categories:
|
|
311
|
+
continue
|
|
312
|
+
|
|
233
313
|
header += "| %*s " % (
|
|
234
|
-
columnWidths.title(
|
|
235
|
-
|
|
314
|
+
columnWidths.title(category),
|
|
315
|
+
decorate_title(category, TITLES[category], options),
|
|
236
316
|
)
|
|
237
|
-
sub_header +=
|
|
317
|
+
sub_header += decorate_subheader(category, columnWidths, options)
|
|
238
318
|
tag_str += " | "
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
(tag
|
|
242
|
-
|
|
243
|
-
(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
tag_str += reportMemory(t, options, field=width)
|
|
319
|
+
|
|
320
|
+
for field in ["min", "med", "ave", "max", "total"]:
|
|
321
|
+
t = getattr(tag, f"{LONG_FORMS[field]}_{category}")
|
|
322
|
+
width = columnWidths.get_width(category, field)
|
|
323
|
+
s = report(t, category, options, field=width)
|
|
324
|
+
tag_str += s
|
|
325
|
+
|
|
247
326
|
out_str += header + "\n"
|
|
248
327
|
out_str += sub_header + "\n"
|
|
249
328
|
out_str += tag_str + "\n"
|
|
250
329
|
return out_str
|
|
251
330
|
|
|
252
331
|
|
|
253
|
-
def
|
|
254
|
-
"""
|
|
255
|
-
|
|
332
|
+
def decorate_title(category: str, title: str, options: Namespace) -> str:
|
|
333
|
+
"""
|
|
334
|
+
Add extra parts to the category titles.
|
|
335
|
+
|
|
336
|
+
Add units to title if they won't appear in the formatted values.
|
|
337
|
+
Add a marker to TITLE if the TITLE is sorted on.
|
|
338
|
+
"""
|
|
339
|
+
unit = CATEGORY_UNITS.get(category)
|
|
340
|
+
if unit in ("s", "core-s") and not options.pretty:
|
|
341
|
+
# This is a time and we won't write it out as text, so add a unit.
|
|
342
|
+
title = f"{title} ({report_unit(unit)})"
|
|
343
|
+
elif unit == "core-s" and options.pretty:
|
|
344
|
+
# This is a core-second category and we won't be putting the core unit
|
|
345
|
+
# in the value, so note that here.
|
|
346
|
+
title = f"{title} (core)"
|
|
347
|
+
elif unit in ("B", "KiB"):
|
|
348
|
+
# The Ki part will appear in the cell so we need a B
|
|
349
|
+
title = f"{title} (B)"
|
|
350
|
+
if category.lower() == options.sortCategory:
|
|
256
351
|
return "%s*" % title
|
|
257
352
|
else:
|
|
258
353
|
return title
|
|
259
354
|
|
|
260
355
|
|
|
261
|
-
def
|
|
262
|
-
|
|
356
|
+
def decorate_subheader(
|
|
357
|
+
category: str, columnWidths: ColumnWidths, options: Namespace
|
|
263
358
|
) -> str:
|
|
264
359
|
"""Add a marker to the correct field if the TITLE is sorted on."""
|
|
265
|
-
|
|
266
|
-
if title != options.sortCategory:
|
|
360
|
+
if category != options.sortCategory:
|
|
267
361
|
s = "| %*s%*s%*s%*s%*s " % (
|
|
268
|
-
columnWidths.
|
|
362
|
+
columnWidths.get_width(category, "min"),
|
|
269
363
|
"min",
|
|
270
|
-
columnWidths.
|
|
364
|
+
columnWidths.get_width(category, "med"),
|
|
271
365
|
"med",
|
|
272
|
-
columnWidths.
|
|
366
|
+
columnWidths.get_width(category, "ave"),
|
|
273
367
|
"ave",
|
|
274
|
-
columnWidths.
|
|
368
|
+
columnWidths.get_width(category, "max"),
|
|
275
369
|
"max",
|
|
276
|
-
columnWidths.
|
|
370
|
+
columnWidths.get_width(category, "total"),
|
|
277
371
|
"total",
|
|
278
372
|
)
|
|
279
373
|
return s
|
|
280
374
|
else:
|
|
281
375
|
s = "| "
|
|
282
376
|
for field, width in [
|
|
283
|
-
("min", columnWidths.
|
|
284
|
-
("med", columnWidths.
|
|
285
|
-
("ave", columnWidths.
|
|
286
|
-
("max", columnWidths.
|
|
287
|
-
("total", columnWidths.
|
|
377
|
+
("min", columnWidths.get_width(category, "min")),
|
|
378
|
+
("med", columnWidths.get_width(category, "med")),
|
|
379
|
+
("ave", columnWidths.get_width(category, "ave")),
|
|
380
|
+
("max", columnWidths.get_width(category, "max")),
|
|
381
|
+
("total", columnWidths.get_width(category, "total")),
|
|
288
382
|
]:
|
|
289
383
|
if options.sortField == field:
|
|
290
384
|
s += "%*s*" % (width - 1, field)
|
|
@@ -302,190 +396,165 @@ def get(tree: Expando, name: str) -> float:
|
|
|
302
396
|
return float("nan")
|
|
303
397
|
|
|
304
398
|
|
|
305
|
-
def
|
|
399
|
+
def sort_jobs(jobTypes: list[Any], options: Namespace) -> list[Any]:
|
|
306
400
|
"""Return a jobTypes all sorted."""
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"ave": "average",
|
|
310
|
-
"min": "min",
|
|
311
|
-
"total": "total",
|
|
312
|
-
"max": "max",
|
|
313
|
-
}
|
|
314
|
-
sortField = longforms[options.sortField]
|
|
315
|
-
if (
|
|
316
|
-
options.sortCategory == "time"
|
|
317
|
-
or options.sortCategory == "clock"
|
|
318
|
-
or options.sortCategory == "wait"
|
|
319
|
-
or options.sortCategory == "memory"
|
|
320
|
-
):
|
|
401
|
+
sortField = LONG_FORMS[options.sortField]
|
|
402
|
+
if options.sortCategory in CATEGORIES:
|
|
321
403
|
return sorted(
|
|
322
404
|
jobTypes,
|
|
323
|
-
key=lambda tag: getattr(
|
|
324
|
-
|
|
405
|
+
key=lambda tag: getattr(
|
|
406
|
+
tag, "{}_{}".format(sortField, options.sortCategory)
|
|
407
|
+
),
|
|
408
|
+
reverse=options.sort == "decending",
|
|
325
409
|
)
|
|
326
410
|
elif options.sortCategory == "alpha":
|
|
327
411
|
return sorted(
|
|
328
412
|
jobTypes,
|
|
329
413
|
key=lambda tag: tag.name, # type: ignore
|
|
330
|
-
reverse=options.
|
|
414
|
+
reverse=options.sort == "decending",
|
|
331
415
|
)
|
|
332
416
|
elif options.sortCategory == "count":
|
|
333
417
|
return sorted(
|
|
334
418
|
jobTypes,
|
|
335
419
|
key=lambda tag: tag.total_number, # type: ignore
|
|
336
|
-
reverse=options.
|
|
420
|
+
reverse=options.sort == "decending",
|
|
337
421
|
)
|
|
338
422
|
|
|
339
423
|
# due to https://stackoverflow.com/questions/47149154
|
|
340
424
|
assert False
|
|
341
425
|
|
|
342
426
|
|
|
343
|
-
def
|
|
427
|
+
def report_pretty_data(
|
|
344
428
|
root: Expando,
|
|
345
|
-
worker:
|
|
346
|
-
job:
|
|
347
|
-
job_types:
|
|
429
|
+
worker: Expando,
|
|
430
|
+
job: Expando,
|
|
431
|
+
job_types: list[Any],
|
|
348
432
|
options: Namespace,
|
|
349
433
|
) -> str:
|
|
350
434
|
"""Print the important bits out."""
|
|
351
435
|
out_str = "Batch System: %s\n" % root.batch_system
|
|
352
436
|
out_str += "Default Cores: %s Default Memory: %s\n" "Max Cores: %s\n" % (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
437
|
+
report_number(n=get(root, "default_cores")),
|
|
438
|
+
# Although per-job memory usage is in KiB, our default is stored in bytes.
|
|
439
|
+
report_space(get(root, "default_memory"), options, unit="B", alone=True),
|
|
440
|
+
report_number(n=get(root, "max_cores"), nan_value="unlimited"),
|
|
356
441
|
)
|
|
357
|
-
out_str += "
|
|
358
|
-
|
|
359
|
-
|
|
442
|
+
out_str += "Local CPU Time: {} Overall Runtime: {}\n".format(
|
|
443
|
+
report(get(root, "total_clock"), "clock", options, alone=True),
|
|
444
|
+
report(get(root, "total_run_time"), "time", options, alone=True),
|
|
360
445
|
)
|
|
361
|
-
job_types =
|
|
362
|
-
columnWidths =
|
|
446
|
+
job_types = sort_jobs(job_types, options)
|
|
447
|
+
columnWidths = compute_column_widths(job_types, worker, job, options)
|
|
363
448
|
out_str += "Worker\n"
|
|
364
|
-
out_str +=
|
|
449
|
+
out_str += sprint_tag("worker", worker, options, columnWidths=columnWidths)
|
|
365
450
|
out_str += "Job\n"
|
|
366
|
-
out_str +=
|
|
451
|
+
out_str += sprint_tag("job", job, options, columnWidths=columnWidths)
|
|
367
452
|
for t in job_types:
|
|
368
453
|
out_str += f" {t.name}\n"
|
|
369
454
|
out_str += f" Total Cores: {t.total_cores}\n"
|
|
370
|
-
out_str +=
|
|
455
|
+
out_str += sprint_tag(t.name, t, options, columnWidths=columnWidths)
|
|
371
456
|
return out_str
|
|
372
457
|
|
|
373
458
|
|
|
374
|
-
def
|
|
375
|
-
job_types:
|
|
459
|
+
def compute_column_widths(
|
|
460
|
+
job_types: list[Any], worker: Expando, job: Expando, options: Namespace
|
|
376
461
|
) -> ColumnWidths:
|
|
377
462
|
"""Return a ColumnWidths() object with the correct max widths."""
|
|
378
463
|
cw = ColumnWidths()
|
|
379
464
|
for t in job_types:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
465
|
+
update_column_widths(t, cw, options)
|
|
466
|
+
update_column_widths(worker, cw, options)
|
|
467
|
+
update_column_widths(job, cw, options)
|
|
383
468
|
return cw
|
|
384
469
|
|
|
385
470
|
|
|
386
|
-
def
|
|
471
|
+
def update_column_widths(tag: Expando, cw: ColumnWidths, options: Namespace) -> None:
|
|
387
472
|
"""Update the column width attributes for this tag's fields."""
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
"ave": "average",
|
|
391
|
-
"min": "min",
|
|
392
|
-
"total": "total",
|
|
393
|
-
"max": "max",
|
|
394
|
-
}
|
|
395
|
-
for category in ["time", "clock", "wait", "memory"]:
|
|
473
|
+
# TODO: Deduplicate with actual printing code!
|
|
474
|
+
for category in CATEGORIES:
|
|
396
475
|
if category in options.categories:
|
|
397
476
|
for field in ["min", "med", "ave", "max", "total"]:
|
|
398
|
-
t = getattr(tag, f"{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
).strip()
|
|
403
|
-
else:
|
|
404
|
-
s = reportMemory(
|
|
405
|
-
t, options, field=cw.getWidth(category, field), isBytes=True
|
|
406
|
-
).strip()
|
|
407
|
-
if len(s) >= cw.getWidth(category, field):
|
|
477
|
+
t = getattr(tag, f"{LONG_FORMS[field]}_{category}")
|
|
478
|
+
width = cw.get_width(category, field)
|
|
479
|
+
s = report(t, category, options, field=width).strip()
|
|
480
|
+
if len(s) >= cw.get_width(category, field):
|
|
408
481
|
# this string is larger than max, width must be increased
|
|
409
|
-
cw.
|
|
482
|
+
cw.set_width(category, field, len(s) + 1)
|
|
410
483
|
|
|
411
484
|
|
|
412
|
-
def
|
|
485
|
+
def build_element(
|
|
486
|
+
element: Expando, items: list[Job], item_name: str, defaults: dict[str, float]
|
|
487
|
+
) -> Expando:
|
|
413
488
|
"""Create an element for output."""
|
|
414
489
|
|
|
415
490
|
def assertNonnegative(i: float, name: str) -> float:
|
|
416
491
|
if i < 0:
|
|
417
|
-
raise RuntimeError("Negative value
|
|
492
|
+
raise RuntimeError("Negative value {} reported for {}".format(i, name))
|
|
418
493
|
else:
|
|
419
494
|
return float(i)
|
|
420
495
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
itemTimes = []
|
|
424
|
-
itemClocks = []
|
|
425
|
-
itemMemory = []
|
|
496
|
+
# Make lists of all values for all items in each category, plus requested cores.
|
|
497
|
+
item_values = {category: [] for category in (CATEGORIES + ["cores"])}
|
|
426
498
|
|
|
427
499
|
for item in items:
|
|
428
500
|
# If something lacks an entry, assume it used none of that thing.
|
|
429
501
|
# This avoids crashing when jobs e.g. aren't done.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
502
|
+
for category, values in item_values.items():
|
|
503
|
+
if category in COMPUTED_CATEGORIES:
|
|
504
|
+
continue
|
|
505
|
+
category_key = category if category != "cores" else "requested_cores"
|
|
506
|
+
category_value = assertNonnegative(
|
|
507
|
+
float(item.get(category_key, defaults[category])), category
|
|
508
|
+
)
|
|
509
|
+
values.append(category_value)
|
|
510
|
+
|
|
511
|
+
for index in range(0, len(item_values[CATEGORIES[0]])):
|
|
512
|
+
# For each item, compute the computed categories
|
|
513
|
+
item_values["wait"].append(
|
|
514
|
+
item_values["time"][index] * item_values["cores"][index]
|
|
515
|
+
- item_values["clock"][index]
|
|
435
516
|
)
|
|
436
517
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
itemMemory.sort()
|
|
447
|
-
|
|
448
|
-
if len(itemTimes) == 0:
|
|
449
|
-
itemTimes.append(0)
|
|
450
|
-
itemClocks.append(0)
|
|
451
|
-
itemWaits.append(0)
|
|
452
|
-
itemMemory.append(0)
|
|
453
|
-
|
|
454
|
-
element[itemName] = Expando(
|
|
455
|
-
total_number=float(len(items)),
|
|
456
|
-
total_time=float(sum(itemTimes)),
|
|
457
|
-
median_time=float(itemTimes[len(itemTimes) // 2]),
|
|
458
|
-
average_time=float(sum(itemTimes) / len(itemTimes)),
|
|
459
|
-
min_time=float(min(itemTimes)),
|
|
460
|
-
max_time=float(max(itemTimes)),
|
|
461
|
-
total_clock=float(sum(itemClocks)),
|
|
462
|
-
median_clock=float(itemClocks[len(itemClocks) // 2]),
|
|
463
|
-
average_clock=float(sum(itemClocks) / len(itemClocks)),
|
|
464
|
-
min_clock=float(min(itemClocks)),
|
|
465
|
-
max_clock=float(max(itemClocks)),
|
|
466
|
-
total_wait=float(sum(itemWaits)),
|
|
467
|
-
median_wait=float(itemWaits[len(itemWaits) // 2]),
|
|
468
|
-
average_wait=float(sum(itemWaits) / len(itemWaits)),
|
|
469
|
-
min_wait=float(min(itemWaits)),
|
|
470
|
-
max_wait=float(max(itemWaits)),
|
|
471
|
-
total_memory=float(sum(itemMemory)),
|
|
472
|
-
median_memory=float(itemMemory[len(itemMemory) // 2]),
|
|
473
|
-
average_memory=float(sum(itemMemory) / len(itemMemory)),
|
|
474
|
-
min_memory=float(min(itemMemory)),
|
|
475
|
-
max_memory=float(max(itemMemory)),
|
|
476
|
-
total_cores=totalCores,
|
|
477
|
-
name=itemName,
|
|
478
|
-
)
|
|
479
|
-
return element[itemName]
|
|
518
|
+
for category, values in item_values.items():
|
|
519
|
+
values.sort()
|
|
520
|
+
|
|
521
|
+
if len(item_values[CATEGORIES[0]]) == 0:
|
|
522
|
+
# Nothing actually there so make a 0 value
|
|
523
|
+
for k, v in item_values.items():
|
|
524
|
+
v.append(0)
|
|
525
|
+
|
|
526
|
+
item_element = Expando(total_number=float(len(items)), name=item_name)
|
|
480
527
|
|
|
528
|
+
for category, values in item_values.items():
|
|
529
|
+
item_element["total_" + category] = float(sum(values))
|
|
530
|
+
item_element["median_" + category] = float(values[len(values) // 2])
|
|
531
|
+
item_element["average_" + category] = float(sum(values) / len(values))
|
|
532
|
+
item_element["min_" + category] = float(min(values))
|
|
533
|
+
item_element["max_" + category] = float(max(values))
|
|
481
534
|
|
|
482
|
-
|
|
535
|
+
element[item_name] = item_element
|
|
536
|
+
|
|
537
|
+
return item_element
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def create_summary(
|
|
483
541
|
element: Expando,
|
|
484
|
-
containingItems:
|
|
542
|
+
containingItems: list[Expando],
|
|
485
543
|
containingItemName: str,
|
|
486
|
-
|
|
544
|
+
count_contained: Callable[[Expando], int],
|
|
487
545
|
) -> None:
|
|
488
|
-
|
|
546
|
+
"""
|
|
547
|
+
Figure out how many jobs (or contained items) ran on each worker (or containing item).
|
|
548
|
+
|
|
549
|
+
Stick a bunch of xxx_number_per_xxx stats into element to describe this.
|
|
550
|
+
|
|
551
|
+
:param count_contained: function that maps from containing item to number of contained items.
|
|
552
|
+
"""
|
|
553
|
+
|
|
554
|
+
# TODO: this still thinks like the old XML stats, even though now the
|
|
555
|
+
# worker records no longer actually contain the job records.
|
|
556
|
+
|
|
557
|
+
itemCounts = [count_contained(containingItem) for containingItem in containingItems]
|
|
489
558
|
itemCounts.sort()
|
|
490
559
|
if len(itemCounts) == 0:
|
|
491
560
|
itemCounts.append(0)
|
|
@@ -499,10 +568,14 @@ def createSummary(
|
|
|
499
568
|
element["max_number_per_%s" % containingItemName] = max(itemCounts)
|
|
500
569
|
|
|
501
570
|
|
|
502
|
-
def
|
|
503
|
-
"""
|
|
571
|
+
def get_stats(jobStore: AbstractJobStore) -> Expando:
|
|
572
|
+
"""
|
|
573
|
+
Sum together all the stats information in the job store.
|
|
504
574
|
|
|
505
|
-
|
|
575
|
+
Produces one object containing lists of the values from all the summed objects.
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
def aggregate_stats(fileHandle: TextIO, aggregateObject: Expando) -> None:
|
|
506
579
|
try:
|
|
507
580
|
stats = json.load(fileHandle, object_hook=Expando)
|
|
508
581
|
for key in list(stats.keys()):
|
|
@@ -517,12 +590,12 @@ def getStats(jobStore: AbstractJobStore) -> Expando:
|
|
|
517
590
|
pass # The file is corrupted.
|
|
518
591
|
|
|
519
592
|
aggregateObject = Expando()
|
|
520
|
-
callBack = partial(
|
|
593
|
+
callBack = partial(aggregate_stats, aggregateObject=aggregateObject)
|
|
521
594
|
jobStore.read_logs(callBack, read_all=True)
|
|
522
595
|
return aggregateObject
|
|
523
596
|
|
|
524
597
|
|
|
525
|
-
def
|
|
598
|
+
def process_data(config: Config, stats: Expando) -> Expando:
|
|
526
599
|
"""
|
|
527
600
|
Collate the stats and report
|
|
528
601
|
"""
|
|
@@ -531,7 +604,11 @@ def processData(config: Config, stats: Expando) -> Expando:
|
|
|
531
604
|
stats.total_time = [0.0]
|
|
532
605
|
stats.total_clock = [0.0]
|
|
533
606
|
|
|
607
|
+
# This is actually the sum of *overall* wall clock time as measured by the
|
|
608
|
+
# leader in each leader invocation, not a sum over jobs.
|
|
534
609
|
stats.total_time = sum(float(number) for number in stats.total_time)
|
|
610
|
+
# And this is CPU clock as measured by the leader, so it will count time
|
|
611
|
+
# used in local jobs but not remote ones.
|
|
535
612
|
stats.total_clock = sum(float(number) for number in stats.total_clock)
|
|
536
613
|
|
|
537
614
|
collatedStatsTag = Expando(
|
|
@@ -540,7 +617,7 @@ def processData(config: Config, stats: Expando) -> Expando:
|
|
|
540
617
|
batch_system=config.batchSystem,
|
|
541
618
|
default_memory=str(config.defaultMemory),
|
|
542
619
|
default_cores=str(config.defaultCores),
|
|
543
|
-
max_cores=str(config.maxCores),
|
|
620
|
+
max_cores=str(config.maxCores if config.maxCores != SYS_MAX_SIZE else None),
|
|
544
621
|
)
|
|
545
622
|
|
|
546
623
|
# Add worker info
|
|
@@ -548,18 +625,16 @@ def processData(config: Config, stats: Expando) -> Expando:
|
|
|
548
625
|
jobs = [_f for _f in getattr(stats, "jobs", []) if _f]
|
|
549
626
|
jobs = [item for sublist in jobs for item in sublist]
|
|
550
627
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
except TypeError:
|
|
555
|
-
return []
|
|
628
|
+
# Work out what usage to assume for things that didn't report
|
|
629
|
+
defaults = {category: 0 for category in CATEGORIES}
|
|
630
|
+
defaults["cores"] = config.defaultCores
|
|
556
631
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
632
|
+
build_element(collatedStatsTag, worker, "worker", defaults)
|
|
633
|
+
create_summary(
|
|
634
|
+
build_element(collatedStatsTag, jobs, "jobs", defaults),
|
|
560
635
|
getattr(stats, "workers", []),
|
|
561
636
|
"worker",
|
|
562
|
-
|
|
637
|
+
lambda worker: getattr(worker, "jobs_run", 0),
|
|
563
638
|
)
|
|
564
639
|
# Get info for each job
|
|
565
640
|
jobNames = set()
|
|
@@ -569,17 +644,17 @@ def processData(config: Config, stats: Expando) -> Expando:
|
|
|
569
644
|
collatedStatsTag.job_types = jobTypesTag
|
|
570
645
|
for jobName in jobNames:
|
|
571
646
|
jobTypes = [job for job in jobs if job.class_name == jobName]
|
|
572
|
-
|
|
647
|
+
build_element(jobTypesTag, jobTypes, jobName, defaults)
|
|
573
648
|
collatedStatsTag.name = "collatedStatsTag"
|
|
574
649
|
return collatedStatsTag
|
|
575
650
|
|
|
576
651
|
|
|
577
|
-
def
|
|
652
|
+
def report_data(tree: Expando, options: Namespace) -> None:
|
|
578
653
|
# Now dump it all out to file
|
|
579
654
|
if options.raw:
|
|
580
655
|
out_str = json.dumps(tree, indent=4, separators=(",", ": "))
|
|
581
656
|
else:
|
|
582
|
-
out_str =
|
|
657
|
+
out_str = report_pretty_data(
|
|
583
658
|
tree, tree.worker, tree.jobs, tree.job_types.values(), options
|
|
584
659
|
)
|
|
585
660
|
if options.outputFile is not None:
|
|
@@ -589,8 +664,7 @@ def reportData(tree: Expando, options: Namespace) -> None:
|
|
|
589
664
|
print(out_str)
|
|
590
665
|
|
|
591
666
|
|
|
592
|
-
|
|
593
|
-
sort_category_choices = ["time", "clock", "wait", "memory", "alpha", "count"]
|
|
667
|
+
sort_category_choices = CATEGORIES + ["alpha", "count"]
|
|
594
668
|
sort_field_choices = ["min", "med", "ave", "max", "total"]
|
|
595
669
|
|
|
596
670
|
|
|
@@ -612,29 +686,28 @@ def add_stats_options(parser: ArgumentParser) -> None:
|
|
|
612
686
|
help="if not raw, prettify the numbers to be human readable.",
|
|
613
687
|
)
|
|
614
688
|
parser.add_argument(
|
|
615
|
-
"--
|
|
616
|
-
"
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
help="Reverse sort.",
|
|
689
|
+
"--sort",
|
|
690
|
+
default="decending",
|
|
691
|
+
choices=["ascending", "decending"],
|
|
692
|
+
help="Sort direction.",
|
|
620
693
|
)
|
|
621
694
|
parser.add_argument(
|
|
622
695
|
"--categories",
|
|
623
|
-
default=",".join(
|
|
696
|
+
default=",".join(CATEGORIES),
|
|
624
697
|
type=str,
|
|
625
|
-
help=f"Comma separated list of any of the following: {
|
|
698
|
+
help=f"Comma separated list of any of the following: {CATEGORIES}.",
|
|
626
699
|
)
|
|
627
700
|
parser.add_argument(
|
|
628
701
|
"--sortCategory",
|
|
629
702
|
default="time",
|
|
630
703
|
choices=sort_category_choices,
|
|
631
|
-
help=f"How to sort job categories.
|
|
704
|
+
help=f"How to sort job categories.",
|
|
632
705
|
)
|
|
633
706
|
parser.add_argument(
|
|
634
707
|
"--sortField",
|
|
635
708
|
default="med",
|
|
636
709
|
choices=sort_field_choices,
|
|
637
|
-
help=f"How to sort job fields.
|
|
710
|
+
help=f"How to sort job fields.",
|
|
638
711
|
)
|
|
639
712
|
|
|
640
713
|
|
|
@@ -645,14 +718,24 @@ def main() -> None:
|
|
|
645
718
|
options = parser.parse_args()
|
|
646
719
|
|
|
647
720
|
for c in options.categories.split(","):
|
|
648
|
-
if c.strip() not in
|
|
649
|
-
|
|
721
|
+
if c.strip().lower() not in CATEGORIES:
|
|
722
|
+
logger.critical(
|
|
723
|
+
"Cannot use category %s, options are: %s", c.strip().lower(), CATEGORIES
|
|
724
|
+
)
|
|
725
|
+
sys.exit(1)
|
|
650
726
|
options.categories = [x.strip().lower() for x in options.categories.split(",")]
|
|
651
727
|
|
|
652
728
|
set_logging_from_options(options)
|
|
653
729
|
config = Config()
|
|
654
730
|
config.setOptions(options)
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
731
|
+
try:
|
|
732
|
+
jobStore = Toil.resumeJobStore(config.jobStore)
|
|
733
|
+
except NoSuchJobStoreException:
|
|
734
|
+
logger.critical("The job store %s does not exist", config.jobStore)
|
|
735
|
+
sys.exit(1)
|
|
736
|
+
logger.info(
|
|
737
|
+
"Gathering stats from jobstore... depending on the number of jobs, this may take a while (e.g. 10 jobs ~= 3 seconds; 100,000 jobs ~= 3,000 seconds or 50 minutes)."
|
|
738
|
+
)
|
|
739
|
+
stats = get_stats(jobStore)
|
|
740
|
+
collatedStatsTag = process_data(jobStore.config, stats)
|
|
741
|
+
report_data(collatedStatsTag, options)
|