1#!/usr/bin/python3
2
3# Copyright (C) 2023 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
17"""Performance stats proto parser utils."""
18
19import datetime
20from typing import Optional, TypeVar
21from . import carwatchdog_dump_parser
22from . import deviceperformancestats_pb2
23from . import performancestats_pb2
24
25
26def _create_date_pb(date: datetime.datetime) -> performancestats_pb2.Date:
27  return performancestats_pb2.Date(
28      year=date.year, month=date.month, day=date.day
29  )
30
31
32def _create_timeofday_pb(date) -> performancestats_pb2.TimeOfDay:
33  return performancestats_pb2.TimeOfDay(
34      hours=date.hour, minutes=date.minute, seconds=date.second
35  )
36
37
38def _add_system_event_pb(
39    system_event_stats: carwatchdog_dump_parser.SystemEventStats,
40    system_event_pb: performancestats_pb2.SystemEventStats,
41) -> None:
42  """Adds the parser SystemEventStats object to the proto object."""
43  for collection in system_event_stats.collections:
44    stats_collection_pb = performancestats_pb2.StatsCollection(
45        id=collection.id,
46        date=_create_date_pb(collection.date),
47        time=_create_timeofday_pb(collection.date),
48        total_cpu_time_ms=collection.total_cpu_time_ms,
49        total_cpu_cycles=collection.total_cpu_cycles,
50        idle_cpu_time_ms=collection.idle_cpu_time_ms,
51        io_wait_time_ms=collection.io_wait_time_ms,
52        context_switches=collection.context_switches,
53        io_blocked_processes=collection.io_blocked_processes,
54        major_page_faults=collection.major_page_faults,
55    )
56
57    for package_cpu_stats in collection.package_cpu_stats:
58      package_cpu_stats_pb = performancestats_pb2.PackageCpuStats(
59          user_id=package_cpu_stats.user_id,
60          package_name=package_cpu_stats.package_name,
61          cpu_time_ms=package_cpu_stats.cpu_time_ms,
62          total_cpu_time_percent=package_cpu_stats.total_cpu_time_percent,
63          cpu_cycles=package_cpu_stats.cpu_cycles,
64      )
65
66      for process_cpu_stats in package_cpu_stats.process_cpu_stats:
67        package_cpu_stats_pb.process_cpu_stats.append(
68            performancestats_pb2.ProcessCpuStats(
69                command=process_cpu_stats.command,
70                cpu_time_ms=process_cpu_stats.cpu_time_ms,
71                package_cpu_time_percent=(
72                    process_cpu_stats.package_cpu_time_percent
73                ),
74                cpu_cycles=process_cpu_stats.cpu_cycles,
75            )
76        )
77
78    stats_collection_pb.package_cpu_stats.append(package_cpu_stats_pb)
79    system_event_pb.collections.append(stats_collection_pb)
80
81    for (
82        package_storage_io_read_stats
83    ) in collection.package_storage_io_read_stats:
84      stats_collection_pb.package_storage_io_read_stats.append(
85          performancestats_pb2.PackageStorageIoStats(
86              user_id=package_storage_io_read_stats.user_id,
87              package_name=package_storage_io_read_stats.package_name,
88              fg_bytes=package_storage_io_read_stats.fg_bytes,
89              fg_bytes_percent=package_storage_io_read_stats.fg_bytes_percent,
90              fg_fsync=package_storage_io_read_stats.fg_fsync,
91              fg_fsync_percent=package_storage_io_read_stats.fg_fsync_percent,
92              bg_bytes=package_storage_io_read_stats.bg_bytes,
93              bg_bytes_percent=package_storage_io_read_stats.bg_bytes_percent,
94              bg_fsync=package_storage_io_read_stats.bg_fsync,
95              bg_fsync_percent=package_storage_io_read_stats.bg_fsync_percent,
96          )
97      )
98
99    for (
100        package_storage_io_write_stats
101    ) in collection.package_storage_io_write_stats:
102      stats_collection_pb.package_storage_io_read_stats.append(
103          performancestats_pb2.PackageStorageIoStats(
104              user_id=package_storage_io_write_stats.user_id,
105              package_name=package_storage_io_write_stats.package_name,
106              fg_bytes=package_storage_io_write_stats.fg_bytes,
107              fg_bytes_percent=package_storage_io_write_stats.fg_bytes_percent,
108              fg_fsync=package_storage_io_write_stats.fg_fsync,
109              fg_fsync_percent=package_storage_io_write_stats.fg_fsync_percent,
110              bg_bytes=package_storage_io_write_stats.bg_bytes,
111              bg_bytes_percent=package_storage_io_write_stats.bg_bytes_percent,
112              bg_fsync=package_storage_io_write_stats.bg_fsync,
113              bg_fsync_percent=package_storage_io_write_stats.bg_fsync_percent,
114          )
115      )
116
117
118def _get_system_event(
119    system_event_pb: performancestats_pb2.SystemEventStats,
120) -> Optional[carwatchdog_dump_parser.SystemEventStats]:
121  """Generates carwatchdog_dump_parser.SystemEventStats from the given proto."""
122  if not system_event_pb.collections:
123    return None
124
125  system_event_stats = carwatchdog_dump_parser.SystemEventStats()
126  for stats_collection_pb in system_event_pb.collections:
127    stats_collection = carwatchdog_dump_parser.StatsCollection()
128    stats_collection.id = stats_collection_pb.id
129    date_pb = stats_collection_pb.date
130    time_pb = stats_collection_pb.time
131    stats_collection.date = datetime.datetime(
132        date_pb.year,
133        date_pb.month,
134        date_pb.day,
135        time_pb.hours,
136        time_pb.minutes,
137        time_pb.seconds,
138    )
139    stats_collection.total_cpu_time_ms = stats_collection_pb.total_cpu_time_ms
140    stats_collection.total_cpu_cycles = stats_collection_pb.total_cpu_cycles
141    stats_collection.idle_cpu_time_ms = stats_collection_pb.idle_cpu_time_ms
142    stats_collection.io_wait_time_ms = stats_collection_pb.io_wait_time_ms
143    stats_collection.context_switches = stats_collection_pb.context_switches
144    stats_collection.io_blocked_processes = (
145        stats_collection_pb.io_blocked_processes
146    )
147    stats_collection.major_page_faults = stats_collection_pb.major_page_faults
148
149    for package_cpu_stats_pb in stats_collection_pb.package_cpu_stats:
150      package_cpu_stats = carwatchdog_dump_parser.PackageCpuStats.from_proto(
151          package_cpu_stats_pb
152      )
153      for process_cpu_stats_pb in package_cpu_stats_pb.process_cpu_stats:
154        package_cpu_stats.process_cpu_stats.append(
155            carwatchdog_dump_parser.ProcessCpuStats.from_proto(
156                process_cpu_stats_pb
157            )
158        )
159      stats_collection.package_cpu_stats.append(package_cpu_stats)
160
161    for (
162        package_storage_io_read_stats_pb
163    ) in stats_collection_pb.package_storage_io_read_stats:
164      stats_collection.package_storage_io_read_stats.append(
165          carwatchdog_dump_parser.PackageStorageIoStats.from_proto(
166              package_storage_io_read_stats_pb
167          )
168      )
169
170    for (
171        package_storage_io_write_stats_pb
172    ) in stats_collection_pb.package_storage_io_write_stats:
173      stats_collection.package_storage_io_write_stats.append(
174          carwatchdog_dump_parser.PackageStorageIoStats.from_proto(
175              package_storage_io_write_stats_pb
176          )
177      )
178
179    system_event_stats.add(stats_collection)
180
181  return system_event_stats
182
183
184def _get_perf_stats(
185    perf_stats_pb: performancestats_pb2.PerformanceStats,
186) -> carwatchdog_dump_parser.PerformanceStats:
187  """Generates carwatchdog_dump_parser.PerformanceStats from the given proto."""
188  perf_stats = carwatchdog_dump_parser.PerformanceStats()
189  perf_stats.boot_time_stats = _get_system_event(perf_stats_pb.boot_time_stats)
190  perf_stats.last_n_minutes_stats = _get_system_event(
191      perf_stats_pb.last_n_minutes_stats
192  )
193  perf_stats.custom_collection_stats = _get_system_event(
194      perf_stats_pb.custom_collection_stats
195  )
196  return perf_stats
197
198
199def _get_build_info(
200    build_info_pb: deviceperformancestats_pb2.BuildInformation,
201) -> carwatchdog_dump_parser.BuildInformation:
202  """Generates carwatchdog_dump_parser.BuildInformation from the given proto."""
203  build_info = carwatchdog_dump_parser.BuildInformation()
204  build_info.fingerprint = build_info_pb.fingerprint
205  build_info.brand = build_info_pb.brand
206  build_info.product = build_info_pb.product
207  build_info.device = build_info_pb.device
208  build_info.version_release = build_info_pb.version_release
209  build_info.id = build_info_pb.id
210  build_info.version_incremental = build_info_pb.version_incremental
211  build_info.type = build_info_pb.type
212  build_info.tags = build_info_pb.tags
213  build_info.sdk = build_info_pb.sdk
214  build_info.platform_minor = build_info_pb.platform_minor
215  build_info.codename = build_info_pb.codename
216  return build_info
217
218
219def write_pb(
220    perf_stats: carwatchdog_dump_parser.PerformanceStats,
221    out_file: str,
222    build_info: Optional[carwatchdog_dump_parser.BuildInformation] = None,
223    out_build_file: Optional[str] = None,
224) -> bool:
225  """Generates proto from parser objects and writes text proto to out files."""
226  if perf_stats.is_empty():
227    print("Cannot write proto since performance stats are empty")
228    return False
229
230  perf_stats_pb = performancestats_pb2.PerformanceStats()
231
232  # Boot time proto
233  if (stats := perf_stats.get_boot_time_stats()) is not None:
234    boot_time_stats_pb = performancestats_pb2.SystemEventStats()
235    _add_system_event_pb(stats, boot_time_stats_pb)
236    perf_stats_pb.boot_time_stats.CopyFrom(boot_time_stats_pb)
237
238  if (stats := perf_stats.get_last_n_minutes_stats()) is not None:
239    last_n_minutes_stats_pb = performancestats_pb2.SystemEventStats()
240    _add_system_event_pb(stats, last_n_minutes_stats_pb)
241    perf_stats_pb.last_n_minutes_stats.CopyFrom(last_n_minutes_stats_pb)
242
243  # TODO(b/256654082): Add user switch events to proto
244
245  # Custom collection proto
246  if (stats := perf_stats.get_custom_collection_stats()) is not None:
247    custom_collection_stats_pb = performancestats_pb2.SystemEventStats()
248    _add_system_event_pb(stats, custom_collection_stats_pb)
249    perf_stats_pb.custom_collection_stats.CopyFrom(custom_collection_stats_pb)
250
251  # Write pb binary to disk
252  if out_file:
253    with open(out_file, "wb") as f:
254      f.write(perf_stats_pb.SerializeToString())
255
256  if build_info is not None:
257    build_info_pb = deviceperformancestats_pb2.BuildInformation(
258        fingerprint=build_info.fingerprint,
259        brand=build_info.brand,
260        product=build_info.product,
261        device=build_info.device,
262        version_release=build_info.version_release,
263        id=build_info.id,
264        version_incremental=build_info.version_incremental,
265        type=build_info.type,
266        tags=build_info.tags,
267        sdk=build_info.sdk,
268        platform_minor=build_info.platform_minor,
269        codename=build_info.codename,
270    )
271
272    device_run_perf_stats_pb = (
273        deviceperformancestats_pb2.DevicePerformanceStats()
274    )
275    device_run_perf_stats_pb.build_info.CopyFrom(build_info_pb)
276    device_run_perf_stats_pb.perf_stats.add().CopyFrom(perf_stats_pb)
277
278    with open(out_build_file, "wb") as f:
279      f.write(device_run_perf_stats_pb.SerializeToString())
280
281  return True
282
283
284T = TypeVar("T")
285
286
287def _read_proto_from_file(pb_file: str, proto: T) -> Optional[T]:
288  """Reads the text proto from the given file and returns the proto object."""
289  pb_type = (
290      "DevicePerformanceStats"
291      if isinstance(proto, deviceperformancestats_pb2.DevicePerformanceStats)
292      else "PerformanceStats"
293  )
294  with open(pb_file, "rb") as f:
295    try:
296      proto.ParseFromString(f.read())
297      proto.DiscardUnknownFields()
298    except UnicodeDecodeError:
299      print(f"Error: Proto in {pb_file} probably is not '{pb_type}'")
300      return None
301
302  if not proto:
303    print(f"Error: Proto stored in {pb_file} has incorrect format.")
304    return None
305
306  return proto
307
308
309def read_performance_stats_pb(
310    pb_file: str,
311) -> Optional[carwatchdog_dump_parser.PerformanceStats]:
312  """Reads text proto from file and returns a PerformanceStats object."""
313  performance_stats_pb = performancestats_pb2.PerformanceStats()
314  performance_stats_pb = _read_proto_from_file(pb_file, performance_stats_pb)
315  if performance_stats_pb is None:
316    return None
317  return _get_perf_stats(performance_stats_pb)
318
319
320def read_device_performance_stats_pb(
321    pb_file: str,
322) -> Optional[carwatchdog_dump_parser.DevicePerformanceStats]:
323  """Reads text proto from file and returns a DevicePerformanceStats object."""
324
325  device_performance_stats_pb = (
326      deviceperformancestats_pb2.DevicePerformanceStats()
327  )
328  device_performance_stats_pb = _read_proto_from_file(
329      pb_file, device_performance_stats_pb
330  )
331  if device_performance_stats_pb is None:
332    return None
333
334  device_run_perf_stats = carwatchdog_dump_parser.DevicePerformanceStats()
335  device_run_perf_stats.build_info = _get_build_info(
336      device_performance_stats_pb.build_info
337  )
338
339  for perf_stat in device_performance_stats_pb.perf_stats:
340    device_run_perf_stats.perf_stats.append(_get_perf_stats(perf_stat))
341
342  return device_run_perf_stats
343