stats.py 10.1 KB
Newer Older
André Anjos's avatar
André Anjos committed
1
2
3
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :

Samuel GAIST's avatar
Samuel GAIST committed
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
###################################################################################
#                                                                                 #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
# Contact: beat.support@idiap.ch                                                  #
#                                                                                 #
# Redistribution and use in source and binary forms, with or without              #
# modification, are permitted provided that the following conditions are met:     #
#                                                                                 #
# 1. Redistributions of source code must retain the above copyright notice, this  #
# list of conditions and the following disclaimer.                                #
#                                                                                 #
# 2. Redistributions in binary form must reproduce the above copyright notice,    #
# this list of conditions and the following disclaimer in the documentation       #
# and/or other materials provided with the distribution.                          #
#                                                                                 #
# 3. Neither the name of the copyright holder nor the names of its contributors   #
# may be used to endorse or promote products derived from this software without   #
# specific prior written permission.                                              #
#                                                                                 #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
#                                                                                 #
###################################################################################

André Anjos's avatar
André Anjos committed
36

37
38
39
40
41
42
"""
=====
stats
=====

A class that can read, validate and update statistical information
43
44
45
46

Forward impored from :py:mod:`beat.backend.python.stats`:
:py:func:`beat.backend.python.stats.io_statistics`
:py:func:`beat.backend.python.stats.update`
47
"""
André Anjos's avatar
André Anjos committed
48
import copy
Samuel GAIST's avatar
Samuel GAIST committed
49
import os
André Anjos's avatar
André Anjos committed
50

51
import simplejson as json
André Anjos's avatar
André Anjos committed
52

53
54
from beat.backend.python.stats import io_statistics  # noqa
from beat.backend.python.stats import update  # noqa
55

Samuel GAIST's avatar
Samuel GAIST committed
56
57
58
from . import prototypes
from . import schema

59

André Anjos's avatar
André Anjos committed
60
class Statistics(object):
Philip ABBET's avatar
Philip ABBET committed
61
    """Statistics define resource usage for algorithmic code runs
André Anjos's avatar
André Anjos committed
62
63


Philip ABBET's avatar
Philip ABBET committed
64
    Parameters:
André Anjos's avatar
André Anjos committed
65

André Anjos's avatar
André Anjos committed
66
      data (:py:class:`object`, Optional): The piece of data representing the
Philip ABBET's avatar
Philip ABBET committed
67
68
69
        statistics the be read, it must validate against our pre-defined
        execution schema. If the input is ``None`` or empty, then start a new
        statistics from scratch.
André Anjos's avatar
André Anjos committed
70
71


Philip ABBET's avatar
Philip ABBET committed
72
    Attributes:
André Anjos's avatar
André Anjos committed
73

74
      errors (list): A list strings containing errors found while loading this
Philip ABBET's avatar
Philip ABBET committed
75
        statistics information.
André Anjos's avatar
André Anjos committed
76

Philip ABBET's avatar
Philip ABBET committed
77
      data (dict): The original data for these statistics
André Anjos's avatar
André Anjos committed
78

Philip ABBET's avatar
Philip ABBET committed
79
    """
André Anjos's avatar
André Anjos committed
80

Philip ABBET's avatar
Philip ABBET committed
81
    def __init__(self, data=None):
André Anjos's avatar
André Anjos committed
82

Philip ABBET's avatar
Philip ABBET committed
83
        self.errors = []
André Anjos's avatar
André Anjos committed
84

Philip ABBET's avatar
Philip ABBET committed
85
        if data:
86
            self._load(data)  # also runs validation
Philip ABBET's avatar
Philip ABBET committed
87
        else:
Samuel GAIST's avatar
Samuel GAIST committed
88
            self._data, self.errors = prototypes.load("statistics")  # also validates
André Anjos's avatar
André Anjos committed
89

Philip ABBET's avatar
Philip ABBET committed
90
91
    def _load(self, data):
        """Loads the statistics
André Anjos's avatar
André Anjos committed
92

Philip ABBET's avatar
Philip ABBET committed
93
        Parameters:
André Anjos's avatar
André Anjos committed
94

95
96
97
          data (object, str, file): The piece of data to load. The input can be
            a valid python object that represents a JSON structure, a file,
            from which the JSON contents will be read out or a string. See
Philip ABBET's avatar
Philip ABBET committed
98
99
            :py:func:`schema.validate` for more details.
        """
André Anjos's avatar
André Anjos committed
100

Philip ABBET's avatar
Philip ABBET committed
101
102
103
        # reset
        self._data = None
        self.errors = []
André Anjos's avatar
André Anjos committed
104

105
        if not isinstance(data, dict):  # user has passed a file pointer
Philip ABBET's avatar
Philip ABBET committed
106
            if not os.path.exists(data):
Samuel GAIST's avatar
Samuel GAIST committed
107
                self.errors.append("File not found: %s" % data)
Philip ABBET's avatar
Philip ABBET committed
108
                return
André Anjos's avatar
André Anjos committed
109

Philip ABBET's avatar
Philip ABBET committed
110
        # this runs basic validation, including JSON loading if required
Samuel GAIST's avatar
Samuel GAIST committed
111
112
113
        self._data, self.errors = schema.validate("statistics", data)
        if self.errors:
            return  # don't proceed with the rest of validation
André Anjos's avatar
André Anjos committed
114

Philip ABBET's avatar
Philip ABBET committed
115
116
117
    @property
    def schema_version(self):
        """Returns the schema version"""
118

Samuel GAIST's avatar
Samuel GAIST committed
119
        return self.data.get("schema_version", 1)
André Anjos's avatar
André Anjos committed
120

Philip ABBET's avatar
Philip ABBET committed
121
122
123
    @property
    def cpu(self):
        """Returns only CPU information"""
124

Samuel GAIST's avatar
Samuel GAIST committed
125
        return self._data["cpu"]
André Anjos's avatar
André Anjos committed
126

Philip ABBET's avatar
Philip ABBET committed
127
128
129
    @cpu.setter
    def cpu(self, data):
        """Sets the CPU information"""
André Anjos's avatar
André Anjos committed
130

Samuel GAIST's avatar
Samuel GAIST committed
131
132
        for key in ("user", "system", "total"):
            self._data["cpu"][key] = data[key]
André Anjos's avatar
André Anjos committed
133

Samuel GAIST's avatar
Samuel GAIST committed
134
135
        for key in ("voluntary", "involuntary"):
            self._data["cpu"]["context_switches"][key] = data["context_switches"][key]
André Anjos's avatar
André Anjos committed
136

Philip ABBET's avatar
Philip ABBET committed
137
138
139
    @property
    def memory(self):
        """Returns only memory information"""
140

Samuel GAIST's avatar
Samuel GAIST committed
141
        return self._data["memory"]
André Anjos's avatar
André Anjos committed
142

Philip ABBET's avatar
Philip ABBET committed
143
144
145
    @memory.setter
    def memory(self, data):
        """Sets only the memory information"""
André Anjos's avatar
André Anjos committed
146

Samuel GAIST's avatar
Samuel GAIST committed
147
148
        for key in ("rss",):
            self._data["memory"][key] = data[key]
André Anjos's avatar
André Anjos committed
149

Philip ABBET's avatar
Philip ABBET committed
150
151
152
    @property
    def data(self):
        """Returns only I/O information"""
153

Samuel GAIST's avatar
Samuel GAIST committed
154
        return self._data["data"]
André Anjos's avatar
André Anjos committed
155

Philip ABBET's avatar
Philip ABBET committed
156
157
158
    @data.setter
    def data(self, data):
        """Sets only the I/O information"""
André Anjos's avatar
André Anjos committed
159

Samuel GAIST's avatar
Samuel GAIST committed
160
161
162
        for key in ("volume", "blocks", "time"):
            self._data["data"][key]["read"] = data[key]["read"]
            self._data["data"][key]["write"] = data[key]["write"]
André Anjos's avatar
André Anjos committed
163

Samuel GAIST's avatar
Samuel GAIST committed
164
165
        self._data["data"]["files"] = list(data["files"])
        self._data["network"] = data["network"]
André Anjos's avatar
André Anjos committed
166

Philip ABBET's avatar
Philip ABBET committed
167
168
169
    @property
    def valid(self):
        """A boolean that indicates if this executor is valid or not"""
André Anjos's avatar
André Anjos committed
170

Philip ABBET's avatar
Philip ABBET committed
171
        return not bool(self.errors)
André Anjos's avatar
André Anjos committed
172

Philip ABBET's avatar
Philip ABBET committed
173
174
    def __add__(self, other):
        """Adds two statistics data blocks"""
André Anjos's avatar
André Anjos committed
175

Philip ABBET's avatar
Philip ABBET committed
176
177
178
        retval = Statistics(copy.deepcopy(self._data))
        retval += other
        return retval
André Anjos's avatar
André Anjos committed
179

Philip ABBET's avatar
Philip ABBET committed
180
181
    def __iadd__(self, other):
        """Self-add statistics from another block"""
André Anjos's avatar
André Anjos committed
182

Samuel GAIST's avatar
Samuel GAIST committed
183
184
        if not isinstance(other, Statistics):
            return NotImplemented
André Anjos's avatar
André Anjos committed
185

Samuel GAIST's avatar
Samuel GAIST committed
186
187
        for key in ("user", "system", "total"):
            self._data["cpu"][key] += other._data["cpu"][key]
André Anjos's avatar
André Anjos committed
188

Samuel GAIST's avatar
Samuel GAIST committed
189
190
191
192
        for key in ("voluntary", "involuntary"):
            self._data["cpu"]["context_switches"][key] += other._data["cpu"][
                "context_switches"
            ][key]
André Anjos's avatar
André Anjos committed
193

Samuel GAIST's avatar
Samuel GAIST committed
194
195
196
197
        for key in ("rss",):  # gets the maximum between the two
            self._data["memory"][key] = max(
                other._data["memory"][key], self._data["memory"][key]
            )
André Anjos's avatar
André Anjos committed
198

Samuel GAIST's avatar
Samuel GAIST committed
199
200
201
        for key in ("volume", "blocks", "time"):
            self._data["data"][key]["read"] += other._data["data"][key]["read"]
            self._data["data"][key]["write"] += other._data["data"][key]["write"]
André Anjos's avatar
André Anjos committed
202

Samuel GAIST's avatar
Samuel GAIST committed
203
        self._data["data"]["files"] += other._data["data"]["files"]
André Anjos's avatar
André Anjos committed
204

Samuel GAIST's avatar
Samuel GAIST committed
205
206
207
        self._data["data"]["network"]["wait_time"] += other._data["data"]["network"][
            "wait_time"
        ]
André Anjos's avatar
André Anjos committed
208

Philip ABBET's avatar
Philip ABBET committed
209
        return self
André Anjos's avatar
André Anjos committed
210

Philip ABBET's avatar
Philip ABBET committed
211
    def __str__(self):
André Anjos's avatar
André Anjos committed
212

Philip ABBET's avatar
Philip ABBET committed
213
        return self.as_json(2)
André Anjos's avatar
André Anjos committed
214

Philip ABBET's avatar
Philip ABBET committed
215
    def as_json(self, indent=None):
216
217
218
219
220
221
222
223
        """Returns self as as JSON

        Parameters:
            :param indent int: Indentation to use for the JSON generation

        Returns:
            dict: JSON representation
        """
André Anjos's avatar
André Anjos committed
224

225
        return json.dumps(self._data, indent=indent)
André Anjos's avatar
André Anjos committed
226

Philip ABBET's avatar
Philip ABBET committed
227
    def as_dict(self):
228
        """Returns self as a dictionary"""
André Anjos's avatar
André Anjos committed
229

Philip ABBET's avatar
Philip ABBET committed
230
        return self._data
André Anjos's avatar
André Anjos committed
231

Philip ABBET's avatar
Philip ABBET committed
232
233
    def write(self, f):
        """Writes contents to a file-like object"""
André Anjos's avatar
André Anjos committed
234

Samuel GAIST's avatar
Samuel GAIST committed
235
236
        if hasattr(f, "write"):
            f.write(str(self))
Philip ABBET's avatar
Philip ABBET committed
237
        else:
Samuel GAIST's avatar
Samuel GAIST committed
238
239
            with open(f, "wt") as fobj:
                fobj.write(str(self))
André Anjos's avatar
André Anjos committed
240
241


242
# ----------------------------------------------------------
André Anjos's avatar
André Anjos committed
243
244


245
def cpu_statistics(start, end):
Philip ABBET's avatar
Philip ABBET committed
246
    """Summarizes current CPU usage
André Anjos's avatar
André Anjos committed
247

Philip ABBET's avatar
Philip ABBET committed
248
249
250
251
    This method should be used when the currently set algorithm is the only one
    executed through the whole process. It is done for collecting resource
    statistics on separate processing environments. It follows the recipe in:
    http://stackoverflow.com/questions/30271942/get-docker-container-cpu-usage-as-percentage
André Anjos's avatar
André Anjos committed
252

Philip ABBET's avatar
Philip ABBET committed
253
    Returns:
André Anjos's avatar
André Anjos committed
254

Philip ABBET's avatar
Philip ABBET committed
255
      dict: A dictionary summarizing current CPU usage
André Anjos's avatar
André Anjos committed
256

Philip ABBET's avatar
Philip ABBET committed
257
    """
André Anjos's avatar
André Anjos committed
258

Samuel GAIST's avatar
Samuel GAIST committed
259
    if "system_cpu_usage" not in end:
Philip ABBET's avatar
Philip ABBET committed
260
        return {
Samuel GAIST's avatar
Samuel GAIST committed
261
262
263
264
265
266
            "user": 0.0,
            "system": 0.0,
            "total": 0.0,
            "percent": 0.0,
            "processors": 1,
        }
Philip ABBET's avatar
Philip ABBET committed
267

Philip ABBET's avatar
Philip ABBET committed
268
    if start is not None:
Samuel GAIST's avatar
Samuel GAIST committed
269
270
        user_cpu = end["cpu_usage"]["total_usage"] - start["cpu_usage"]["total_usage"]
        total_cpu = end["system_cpu_usage"] - start["system_cpu_usage"]
271

Philip ABBET's avatar
Philip ABBET committed
272
    else:
Samuel GAIST's avatar
Samuel GAIST committed
273
274
        user_cpu = end["cpu_usage"]["total_usage"]
        total_cpu = end["system_cpu_usage"]
275

Samuel GAIST's avatar
Samuel GAIST committed
276
277
278
279
280
281
282
    user_cpu /= 1000000000.0  # in seconds
    total_cpu /= 1000000000.0  # in seconds
    processors = (
        len(end["cpu_usage"]["percpu_usage"])
        if end["cpu_usage"]["percpu_usage"] is not None
        else 1
    )
283

Philip ABBET's avatar
Philip ABBET committed
284
    return {
Samuel GAIST's avatar
Samuel GAIST committed
285
286
287
288
289
290
        "user": user_cpu,
        "system": 0.0,
        "total": total_cpu,
        "percent": 100.0 * processors * user_cpu / total_cpu if total_cpu else 0.0,
        "processors": processors,
    }
André Anjos's avatar
André Anjos committed
291
292


293
# ----------------------------------------------------------
294
295


296
def memory_statistics(data):
Philip ABBET's avatar
Philip ABBET committed
297
    """Summarizes current memory usage
André Anjos's avatar
André Anjos committed
298

Philip ABBET's avatar
Philip ABBET committed
299
300
301
    This method should be used when the currently set algorithm is the only one
    executed through the whole process. It is done for collecting resource
    statistics on separate processing environments.
André Anjos's avatar
André Anjos committed
302

Philip ABBET's avatar
Philip ABBET committed
303
    Returns:
André Anjos's avatar
André Anjos committed
304

Philip ABBET's avatar
Philip ABBET committed
305
      dict: A dictionary summarizing current memory usage
André Anjos's avatar
André Anjos committed
306

Philip ABBET's avatar
Philip ABBET committed
307
    """
André Anjos's avatar
André Anjos committed
308

Samuel GAIST's avatar
Samuel GAIST committed
309
310
    limit = float(data["limit"])
    memory = float(data["max_usage"])
André Anjos's avatar
André Anjos committed
311

Philip ABBET's avatar
Philip ABBET committed
312
    return {
Samuel GAIST's avatar
Samuel GAIST committed
313
314
315
316
        "rss": memory,
        "limit": limit,
        "percent": 100.0 * memory / limit if limit else 0.0,
    }