| # |
| # Copyright (C) 2016 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import logging |
| import os |
| |
| from google.protobuf import text_format |
| from vts.proto import ComponentSpecificationMessage_pb2 as CompSpecMsg |
| from vts.proto import VtsProfilingMessage_pb2 as VtsProfilingMsg |
| from vts.proto import VtsReportMessage_pb2 as ReportMsg |
| from vts.runners.host import asserts |
| from vts.runners.host import const |
| from vts.runners.host import keys |
| from vts.utils.python.common import cmd_utils |
| from vts.utils.python.os import path_utils |
| from vts.utils.python.web import feature_utils |
| |
| LOCAL_PROFILING_TRACE_PATH = "/tmp/vts-test-trace" |
| TARGET_PROFILING_TRACE_PATH = "/data/local/tmp/" |
| HAL_INSTRUMENTATION_LIB_PATH_32 = "/data/local/tmp/32/" |
| HAL_INSTRUMENTATION_LIB_PATH_64 = "/data/local/tmp/64/" |
| DEFAULT_HAL_ROOT = "android.hardware." |
| |
| _PROFILING_DATA = "profiling_data" |
| _HOST_PROFILING_DATA = "host_profiling_data" |
| |
| |
| class VTSProfilingData(object): |
| """Class to store the VTS profiling data. |
| |
| Attributes: |
| values: A dict that stores the profiling data. e.g. latencies of each api. |
| options: A set of strings where each string specifies an associated |
| option (which is the form of 'key=value'). |
| """ |
| |
| def __init__(self): |
| self.values = {} |
| self.options = set() |
| |
| |
| EVENT_TYPE_DICT = { |
| 0: "SERVER_API_ENTRY", |
| 1: "SERVER_API_EXIT", |
| 2: "CLIENT_API_ENTRY", |
| 3: "CLIENT_API_EXIT", |
| 4: "SYNC_CALLBACK_ENTRY", |
| 5: "SYNC_CALLBACK_EXIT", |
| 6: "ASYNC_CALLBACK_ENTRY", |
| 7: "ASYNC_CALLBACK_EXIT", |
| 8: "PASSTHROUGH_ENTRY", |
| 9: "PASSTHROUGH_EXIT", |
| } |
| |
| |
| class VTSApiCoverageData(object): |
| """Class to store the API coverage data. |
| |
| Attributes: |
| package_name: sting, HAL package name (e.g. android.hardware.foo). |
| version_major: string, HAL major version (e.g. 1). |
| version_minor: string, HAL minor version (e.g. 0). |
| interface_name: string, HAL interface name (e.g. IFoo). |
| total_apis: A set of strings, each string represents an API name defined |
| in the given HAL. |
| covered_apis: A set of strings, each string represents an API name |
| covered by the test. |
| """ |
| |
| def __init__(self, package_name, version, interface_name): |
| self.package_name = package_name |
| self.version_major, self.version_minor = version.split(".") |
| self.interface_name = interface_name |
| self.total_apis = set() |
| self.covered_apis = set() |
| |
| |
| class ProfilingFeature(feature_utils.Feature): |
| """Feature object for profiling functionality. |
| |
| Attributes: |
| enabled: boolean, True if profiling is enabled, False otherwise |
| web: (optional) WebFeature, object storing web feature util for test run. |
| data_file_path: Path to the data directory within vts package. |
| api_coverage_data: A dictionary from full HAL interface name |
| (e.g. android.hardware.foo@1.0::IFoo) to VtsApiCoverageData. |
| """ |
| |
| _TOGGLE_PARAM = keys.ConfigKeys.IKEY_ENABLE_PROFILING |
| _REQUIRED_PARAMS = [keys.ConfigKeys.IKEY_DATA_FILE_PATH] |
| _OPTIONAL_PARAMS = [ |
| keys.ConfigKeys.IKEY_PROFILING_TRACING_PATH, |
| keys.ConfigKeys.IKEY_TRACE_FILE_TOOL_NAME, |
| keys.ConfigKeys.IKEY_SAVE_TRACE_FILE_REMOTE, |
| keys.ConfigKeys.IKEY_ABI_BITNESS, |
| keys.ConfigKeys.IKEY_PROFILING_ARG_VALUE, |
| ] |
| |
| def __init__(self, user_params, web=None): |
| """Initializes the profiling feature. |
| |
| Args: |
| user_params: A dictionary from parameter name (String) to parameter value. |
| web: (optional) WebFeature, object storing web feature util for test run |
| """ |
| self.ParseParameters(self._TOGGLE_PARAM, self._REQUIRED_PARAMS, |
| self._OPTIONAL_PARAMS, user_params) |
| self.web = web |
| if self.enabled: |
| logging.info("Profiling is enabled.") |
| else: |
| logging.debug("Profiling is disabled.") |
| |
| self.data_file_path = getattr(self, |
| keys.ConfigKeys.IKEY_DATA_FILE_PATH, None) |
| self.api_coverage_data = {} |
| |
| def _IsEventFromBinderizedHal(self, event_type): |
| """Returns True if the event type is from a binderized HAL.""" |
| if event_type in [8, 9]: |
| return False |
| return True |
| |
| def GetTraceFiles(self, |
| dut, |
| host_profiling_trace_path=None, |
| trace_file_tool=None): |
| """Pulls the trace file and save it under the profiling trace path. |
| |
| Args: |
| dut: the testing device. |
| host_profiling_trace_path: directory that stores trace files on host. |
| trace_file_tool: tools that used to store the trace file. |
| |
| Returns: |
| Name list of trace files that stored on host. |
| """ |
| if not os.path.exists(LOCAL_PROFILING_TRACE_PATH): |
| os.makedirs(LOCAL_PROFILING_TRACE_PATH) |
| |
| if not host_profiling_trace_path: |
| host_profiling_trace_path = LOCAL_PROFILING_TRACE_PATH |
| |
| target_trace_file = path_utils.JoinTargetPath( |
| TARGET_PROFILING_TRACE_PATH, "*.vts.trace") |
| results = dut.shell.Execute("ls " + target_trace_file) |
| asserts.assertTrue(results, "failed to find trace file") |
| stdout_lines = results[const.STDOUT][0].split("\n") |
| logging.debug("stdout: %s", stdout_lines) |
| trace_files = [] |
| for line in stdout_lines: |
| if line: |
| temp_file_name = os.path.join(LOCAL_PROFILING_TRACE_PATH, |
| os.path.basename(line.strip())) |
| dut.adb.pull("%s %s" % (line, temp_file_name)) |
| trace_file_name = os.path.join(host_profiling_trace_path, |
| os.path.basename(line.strip())) |
| logging.info("Saving profiling traces: %s" % trace_file_name) |
| if temp_file_name != trace_file_name: |
| file_cmd = "" |
| if trace_file_tool: |
| file_cmd += trace_file_tool |
| file_cmd += " cp " + temp_file_name + " " + trace_file_name |
| results = cmd_utils.ExecuteShellCommand(file_cmd) |
| if results[const.EXIT_CODE][0] != 0: |
| logging.error(results[const.STDERR][0]) |
| logging.error("Fail to execute command: %s" % file_cmd) |
| trace_files.append(temp_file_name) |
| return trace_files |
| |
| def EnableVTSProfiling(self, shell, hal_instrumentation_lib_path=None): |
| """ Enable profiling by setting the system property. |
| |
| Args: |
| shell: shell to control the testing device. |
| hal_instrumentation_lib_path: string, the path of directory that stores |
| profiling libraries. |
| """ |
| hal_instrumentation_lib_path_32 = HAL_INSTRUMENTATION_LIB_PATH_32 |
| hal_instrumentation_lib_path_64 = HAL_INSTRUMENTATION_LIB_PATH_64 |
| if hal_instrumentation_lib_path is not None: |
| bitness = getattr(self, keys.ConfigKeys.IKEY_ABI_BITNESS, None) |
| if bitness == '64': |
| hal_instrumentation_lib_path_64 = hal_instrumentation_lib_path |
| elif bitness == '32': |
| hal_instrumentation_lib_path_32 = hal_instrumentation_lib_path |
| else: |
| logging.error('Unknown abi bitness "%s". Using 64bit hal ' |
| 'instrumentation lib path.', bitness) |
| |
| # cleanup any existing traces. |
| shell.Execute( |
| "rm " + os.path.join(TARGET_PROFILING_TRACE_PATH, "*.vts.trace")) |
| logging.debug("enabling VTS profiling.") |
| |
| # give permission to write the trace file. |
| shell.Execute("chmod 777 " + TARGET_PROFILING_TRACE_PATH) |
| |
| shell.Execute("setprop hal.instrumentation.lib.path.32 " + |
| hal_instrumentation_lib_path_32) |
| shell.Execute("setprop hal.instrumentation.lib.path.64 " + |
| hal_instrumentation_lib_path_64) |
| |
| if getattr(self, keys.ConfigKeys.IKEY_PROFILING_ARG_VALUE, False): |
| shell.Execute("setprop hal.instrumentation.profile.args true") |
| else: |
| shell.Execute("setprop hal.instrumentation.profile.args false") |
| shell.Execute("setprop hal.instrumentation.enable true") |
| |
| def DisableVTSProfiling(self, shell): |
| """ Disable profiling by resetting the system property. |
| |
| Args: |
| shell: shell to control the testing device. |
| """ |
| shell.Execute("setprop hal.instrumentation.lib.path \"\"") |
| shell.Execute("setprop hal.instrumentation.profile.args \"\"") |
| shell.Execute("setprop hal.instrumentation.enable false") |
| |
| def _ParseTraceData(self, trace_file, measure_api_coverage): |
| """Parses the data stored in trace_file, calculates the avg/max/min |
| latency for each API. |
| |
| Args: |
| trace_file: file that stores the trace data. |
| measure_api_coverage: whether to measure the api coverage data. |
| |
| Returns: |
| VTSProfilingData which contain the list of API names and the avg/max/min |
| latency for each API. |
| """ |
| profiling_data = VTSProfilingData() |
| api_timestamps = {} |
| api_latencies = {} |
| |
| trace_processor_binary = os.path.join(self.data_file_path, "host", |
| "bin", "trace_processor") |
| trace_processor_lib = os.path.join(self.data_file_path, "host", |
| "lib64") |
| trace_processor_cmd = [ |
| "chmod a+x %s" % trace_processor_binary, |
| "LD_LIBRARY_PATH=%s %s -m profiling_trace %s" % |
| (trace_processor_lib, trace_processor_binary, trace_file) |
| ] |
| |
| results = cmd_utils.ExecuteShellCommand(trace_processor_cmd) |
| if any(results[cmd_utils.EXIT_CODE]): |
| logging.error("Fail to execute command: %s" % trace_processor_cmd) |
| logging.error("stdout: %s" % results[const.STDOUT]) |
| logging.error("stderr: %s" % results[const.STDERR]) |
| return profiling_data |
| |
| stdout_lines = results[const.STDOUT][1].split("\n") |
| first_line = True |
| for line in stdout_lines: |
| if not line: |
| continue |
| if first_line: |
| _, mode = line.split(":") |
| profiling_data.options.add("hidl_hal_mode=%s" % mode) |
| first_line = False |
| else: |
| full_api, latency = line.rsplit(":", 1) |
| full_interface, api_name = full_api.rsplit("::", 1) |
| if profiling_data.values.get(api_name): |
| profiling_data.values[api_name].append(long(latency)) |
| else: |
| profiling_data.values[api_name] = [long(latency)] |
| |
| if measure_api_coverage: |
| package, interface_name = full_interface.split("::") |
| package_name, version = package.split("@") |
| |
| if full_interface in self.api_coverage_data: |
| self.api_coverage_data[ |
| full_interface].covered_apis.add(api_name) |
| else: |
| total_apis = self._GetTotalApis( |
| package_name, version, interface_name) |
| if total_apis: |
| vts_api_coverage = VTSApiCoverageData( |
| package_name, version, interface_name) |
| vts_api_coverage.total_apis = total_apis |
| if api_name in total_apis: |
| vts_api_coverage.covered_apis.add(api_name) |
| else: |
| logging.warning("API %s is not supported by %s", |
| api_name, full_interface) |
| self.api_coverage_data[ |
| full_interface] = vts_api_coverage |
| |
| return profiling_data |
| |
| def _GetTotalApis(self, package_name, version, interface_name): |
| """Parse the specified vts spec and get all APIs defined in the spec. |
| |
| Args: |
| package_name: string, HAL package name. |
| version: string, HAL version. |
| interface_name: string, HAL interface name. |
| |
| Returns: |
| A set of strings, each string represents an API defined in the spec. |
| """ |
| total_apis = set() |
| spec_proto = CompSpecMsg.ComponentSpecificationMessage() |
| # TODO: support general package that does not start with android.hardware. |
| if not package_name.startswith(DEFAULT_HAL_ROOT): |
| logging.warning("Unsupported hal package: %s", package_name) |
| return total_apis |
| |
| hal_package_path = package_name[len(DEFAULT_HAL_ROOT):].replace( |
| ".", "/") |
| vts_spec_path = os.path.join( |
| self.data_file_path, "spec/hardware/interfaces", hal_package_path, |
| version, "vts", interface_name[1:] + ".vts") |
| logging.debug("vts_spec_path: %s", vts_spec_path) |
| with open(vts_spec_path, 'r') as spec_file: |
| spec_string = spec_file.read() |
| text_format.Merge(spec_string, spec_proto) |
| for api in spec_proto.interface.api: |
| if not api.is_inherited: |
| total_apis.add(api.name) |
| return total_apis |
| |
| def StartHostProfiling(self, name): |
| """Starts a profiling operation. |
| |
| Args: |
| name: string, the name of a profiling point |
| |
| Returns: |
| True if successful, False otherwise |
| """ |
| if not self.enabled: |
| return False |
| |
| if not hasattr(self, _HOST_PROFILING_DATA): |
| setattr(self, _HOST_PROFILING_DATA, {}) |
| |
| host_profiling_data = getattr(self, _HOST_PROFILING_DATA) |
| |
| if name in host_profiling_data: |
| logging.error("profiling point %s is already active.", name) |
| return False |
| host_profiling_data[name] = feature_utils.GetTimestamp() |
| return True |
| |
| def StopHostProfiling(self, name): |
| """Stops a profiling operation. |
| |
| Args: |
| name: string, the name of a profiling point |
| """ |
| if not self.enabled: |
| return |
| |
| if not hasattr(self, _HOST_PROFILING_DATA): |
| setattr(self, _HOST_PROFILING_DATA, {}) |
| |
| host_profiling_data = getattr(self, _HOST_PROFILING_DATA) |
| |
| if name not in host_profiling_data: |
| logging.error("profiling point %s is not active.", name) |
| return False |
| |
| start_timestamp = host_profiling_data[name] |
| end_timestamp = feature_utils.GetTimestamp() |
| if self.web and self.web.enabled: |
| self.web.AddProfilingDataTimestamp(name, start_timestamp, |
| end_timestamp) |
| return True |
| |
| def ProcessTraceDataForTestCase(self, dut, measure_api_coverage=True): |
| """Pulls the generated trace file to the host, parses the trace file to |
| get the profiling data (e.g. latency of each API call) and stores these |
| data in _profiling_data. |
| |
| Requires the feature to be enabled; no-op otherwise. |
| |
| Args: |
| dut: the registered device. |
| """ |
| if not self.enabled: |
| return |
| |
| if not hasattr(self, _PROFILING_DATA): |
| setattr(self, _PROFILING_DATA, []) |
| |
| profiling_data = getattr(self, _PROFILING_DATA) |
| |
| trace_files = [] |
| save_trace_remote = getattr( |
| self, keys.ConfigKeys.IKEY_SAVE_TRACE_FILE_REMOTE, False) |
| if save_trace_remote: |
| trace_files = self.GetTraceFiles( |
| dut, |
| getattr(self, keys.ConfigKeys.IKEY_PROFILING_TRACING_PATH, |
| None), |
| getattr(self, keys.ConfigKeys.IKEY_TRACE_FILE_TOOL_NAME, None)) |
| else: |
| trace_files = self.GetTraceFiles(dut) |
| |
| for file in trace_files: |
| logging.info("parsing trace file: %s.", file) |
| data = self._ParseTraceData(file, measure_api_coverage) |
| if data: |
| profiling_data.append(data) |
| |
| def ProcessAndUploadTraceData(self, upload_api_coverage=True): |
| """Process and upload profiling trace data. |
| |
| Requires the feature to be enabled; no-op otherwise. |
| |
| Merges the profiling data generated by each test case, calculates the |
| aggregated max/min/avg latency for each API and uploads these latency |
| metrics to webdb. |
| |
| Args: |
| upload_api_coverage: whether to upload the API coverage data. |
| """ |
| if not self.enabled: |
| return |
| |
| merged_profiling_data = VTSProfilingData() |
| for data in getattr(self, _PROFILING_DATA, []): |
| for item in data.options: |
| merged_profiling_data.options.add(item) |
| for api, latences in data.values.items(): |
| if merged_profiling_data.values.get(api): |
| merged_profiling_data.values[api].extend(latences) |
| else: |
| merged_profiling_data.values[api] = latences |
| for api, latencies in merged_profiling_data.values.items(): |
| if not self.web or not self.web.enabled: |
| continue |
| |
| self.web.AddProfilingDataUnlabeledVector( |
| api, |
| latencies, |
| merged_profiling_data.options, |
| x_axis_label="API processing latency (nano secs)", |
| y_axis_label="Frequency") |
| |
| if upload_api_coverage: |
| self.web.AddApiCoverageReport(self.api_coverage_data.values()) |