• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3#   Copyright 2020 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import logging
18import os
19import uuid
20import tempfile
21import yaml
22from datetime import datetime
23
24from acts.libs.proc import job
25from acts import context
26
27
28class BitsClientError(Exception):
29    pass
30
31
32# An arbitrary large number of seconds.
33ONE_YEAR = str(3600 * 24 * 365)
34EPOCH = datetime.utcfromtimestamp(0)
35
36
37def _to_ns(timestamp):
38    """Returns the numerical value of a timestamp in nanoseconds since epoch.
39
40    Args:
41        timestamp: Either a number or a datetime.
42
43    Returns:
44        Rounded timestamp if timestamp is numeric, number of nanoseconds since
45        epoch if timestamp is instance of datetime.datetime.
46    """
47    if isinstance(timestamp, datetime):
48        return int((timestamp - EPOCH).total_seconds() * 1e9)
49    elif isinstance(timestamp, (float, int)):
50        return int(timestamp)
51    raise ValueError('%s can not be converted to a numerical representation of '
52                     'nanoseconds.' % type(timestamp))
53
54
55class _BitsCollection(object):
56    """Object that represents a bits collection
57
58    Attributes:
59        name: The name given to the collection.
60        markers_buffer: An array of un-flushed markers, each marker is
61        represented by a bi-dimensional tuple with the format
62        (<nanoseconds_since_epoch or datetime>, <text>).
63    """
64    def __init__(self, name):
65        self.name = name
66        self.markers_buffer = []
67
68    def add_marker(self, timestamp, marker_text):
69        self.markers_buffer.append((timestamp, marker_text))
70
71    def clear_markers_buffer(self):
72        self.markers_buffer.clear()
73
74
75class BitsClient(object):
76    """Helper class to issue bits' commands"""
77
78    def __init__(self, binary, service, service_config):
79        """Constructs a BitsClient.
80
81        Args:
82            binary: The location of the bits.par client binary.
83            service: A bits_service.BitsService object. The service is expected
84              to be previously setup.
85            service_config: The bits_service_config.BitsService object used to
86              start the service on service_port.
87        """
88        self._log = logging.getLogger()
89        self._binary = binary
90        self._service = service
91        self._server_config = service_config
92        self._active_collection = None
93        self._collections_counter = 0
94
95    def _acquire_monsoon(self):
96        """Gets hold of a Monsoon so no other processes can use it.
97        Only works if there is a monsoon."""
98        cmd = [self._binary,
99               '--port',
100               self._service.port,
101               '--collector',
102               'Monsoon',
103               '--collector_cmd',
104               'acquire_monsoon']
105        self._log.info('acquiring monsoon')
106        job.run(cmd, timeout=10)
107
108    def _release_monsoon(self):
109        cmd = [self._binary,
110               '--port',
111               self._service.port,
112               '--collector',
113               'Monsoon',
114               '--collector_cmd',
115               'release_monsoon']
116        self._log.info('releasing monsoon')
117        job.run(cmd, timeout=10)
118
119    def _export(self):
120        collection_path = os.path.join(
121            context.get_current_context().get_full_output_path(),
122            '%s.7z.bits' % self._active_collection.name)
123        cmd = [self._binary,
124               '--port',
125               self._service.port,
126               '--name',
127               self._active_collection.name,
128               '--ignore_gaps',
129               '--export',
130               '--export_path',
131               collection_path]
132        self._log.info('exporting collection %s to %s',
133                       self._active_collection.name,
134                       collection_path)
135        job.run(cmd, timeout=600)
136
137    def _flush_markers(self):
138        for ts, marker in sorted(self._active_collection.markers_buffer,
139                                 key=lambda x: x[0]):
140            cmd = [self._binary,
141                   '--port',
142                   self._service.port,
143                   '--name',
144                   self._active_collection.name,
145                   '--log_ts',
146                   str(_to_ns(ts)),
147                   '--log',
148                   marker]
149            job.run(cmd, timeout=10)
150        self._active_collection.clear_markers_buffer()
151
152    def add_marker(self, timestamp, marker_text):
153        """Buffers a marker for the active collection.
154
155        Bits does not allow inserting markers with timestamps out of order.
156        The buffer of markers will be flushed when the collection is stopped to
157        ensure all the timestamps are input in order.
158
159        Args:
160            timestamp: Numerical nanoseconds since epoch or datetime.
161            marker_text: A string to label this marker with.
162        """
163        if not self._active_collection:
164            raise BitsClientError(
165                'markers can not be added without an active collection')
166        self._active_collection.add_marker(timestamp, marker_text)
167
168    def get_metrics(self, start, end):
169        """Extracts metrics for a period of time.
170
171        Args:
172            start: Numerical nanoseconds since epoch until the start of the
173            period of interest or datetime.
174            end: Numerical nanoseconds since epoch until the end of the
175            period of interest or datetime.
176        """
177        if not self._active_collection:
178            raise BitsClientError(
179                'metrics can not be collected without an active collection')
180
181        with tempfile.NamedTemporaryFile(prefix='bits_metrics') as tf:
182            cmd = [self._binary,
183                   '--port',
184                   self._service.port,
185                   '--name',
186                   self._active_collection.name,
187                   '--ignore_gaps',
188                   '--abs_start_time',
189                   str(_to_ns(start)),
190                   '--abs_stop_time',
191                   str(_to_ns(end)),
192                   '--aggregates_yaml_path',
193                   tf.name]
194            if self._server_config.has_virtual_metrics_file:
195                cmd = cmd + ['--vm_file', 'default']
196            job.run(cmd)
197            with open(tf.name) as mf:
198                self._log.debug(
199                    'bits aggregates for collection %s [%s-%s]: %s' % (
200                        self._active_collection.name, start, end,
201                        mf.read()))
202
203            with open(tf.name) as mf:
204                return yaml.safe_load(mf)
205
206    def disconnect_usb(self):
207        """Disconnects the monsoon's usb. Only works if there is a monsoon"""
208        cmd = [self._binary,
209               '--port',
210               self._service.port,
211               '--collector',
212               'Monsoon',
213               '--collector_cmd',
214               'usb_disconnect']
215        self._log.info('disconnecting monsoon\'s usb')
216        job.run(cmd, timeout=10)
217
218    def start_collection(self, postfix=None):
219        """Indicates Bits to start a collection.
220
221        Args:
222            postfix: Optional argument that can be used to identify the
223            collection with.
224        """
225        if self._active_collection:
226            raise BitsClientError(
227                'Attempted to start a collection while there is still an '
228                'active one. Active collection: %s',
229                self._active_collection.name)
230        self._collections_counter = self._collections_counter + 1
231        # The name gets a random 8 characters salt suffix because the Bits
232        # client has a bug where files with the same name are considered to be
233        # the same collection and it won't load two files with the same name.
234        # b/153170987 b/153944171
235        if not postfix:
236            postfix = str(self._collections_counter)
237        postfix = '%s_%s' % (postfix, str(uuid.uuid4())[0:8])
238        self._active_collection = _BitsCollection(
239            'bits_collection_%s' % postfix)
240
241        cmd = [self._binary,
242               '--port',
243               self._service.port,
244               '--name',
245               self._active_collection.name,
246               '--non_blocking',
247               '--time',
248               ONE_YEAR,
249               '--default_sampling_rate',
250               '1000',
251               '--disk_space_saver']
252        self._log.info('starting collection %s', self._active_collection.name)
253        job.run(cmd, timeout=10)
254
255    def connect_usb(self):
256        """Connects the monsoon's usb. Only works if there is a monsoon."""
257        cmd = [self._binary,
258               '--port',
259               self._service.port,
260               '--collector',
261               'Monsoon',
262               '--collector_cmd',
263               'usb_connect']
264        self._log.info('connecting monsoon\'s usb')
265        job.run(cmd, timeout=10)
266
267    def stop_collection(self):
268        """Stops the active collection."""
269        if not self._active_collection:
270            raise BitsClientError(
271                'Attempted to stop a collection without starting one')
272        self._log.info('stopping collection %s', self._active_collection.name)
273        self._flush_markers()
274        cmd = [self._binary,
275               '--port',
276               self._service.port,
277               '--name',
278               self._active_collection.name,
279               '--stop']
280        job.run(cmd)
281        self._export()
282        self._log.info('stopped collection %s', self._active_collection.name)
283        self._active_collection = None
284
285    def list_devices(self):
286        """Lists devices managed by the bits_server this client is connected
287        to.
288
289        Returns:
290            bits' output when called with --list devices.
291        """
292        cmd = [self._binary,
293               '--port',
294               self._service.port,
295               '--list',
296               'devices']
297        self._log.debug('listing devices')
298        result = job.run(cmd, timeout=20)
299        return result.stdout
300