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