• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python3
2
3# Copyright (C) 2022 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"""
18Tool to parse CarWatchdog's performance stats dump.
19
20To build the parser script run:
21  m perf_stats_parser
22
23To parse a carwatchdog dump text file run:
24  perf_stats_parser -f <cw-dump>.txt -o cw_proto_out.pb
25
26To read a carwatchdog proto file as a json run:
27  pers_stats_parser -r <cw-proto-out>.pb -j
28"""
29import argparse
30import json
31import os
32import re
33import sys
34
35from parser import performancestats_pb2
36from parser import deviceperformancestats_pb2
37from datetime import datetime
38
39BOOT_TIME_REPORT_HEADER = "Boot-time performance report:"
40CUSTOM_COLLECTION_REPORT_HEADER = "Custom performance data report:"
41TOP_N_CPU_TIME_HEADER = "Top N CPU Times:"
42
43DUMP_DATETIME_FORMAT = "%a %b %d %H:%M:%S %Y %Z"
44DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
45
46STATS_COLLECTION_PATTERN = "Collection (\d+): <(.+)>"
47PACKAGE_CPU_STATS_PATTERN = "(\d+), (.+), (\d+), (\d+).(\d+)%(, (\d+))?"
48PROCESS_CPU_STATS_PATTERN = "\s+(.+), (\d+), (\d+).(\d+)%(, (\d+))?"
49TOTAL_CPU_TIME_PATTERN = "Total CPU time \\(ms\\): (\d+)"
50TOTAL_IDLE_CPU_TIME_PATTERN = "Total idle CPU time \\(ms\\)/percent: (\d+) / .+"
51CPU_IO_WAIT_TIME_PATTERN = "CPU I/O wait time \\(ms\\)/percent: (\d+) / .+"
52CONTEXT_SWITCHES_PATTERN = "Number of context switches: (\d+)"
53IO_BLOCKED_PROCESSES_PATTERN = "Number of I/O blocked processes/percent: (\d+) / .+"
54
55
56class BuildInformation:
57  def __init__(self):
58    self.fingerprint = None
59    self.brand = None
60    self.product = None
61    self.device = None
62    self.version_release = None
63    self.id = None
64    self.version_incremental = None
65    self.type = None
66    self.tags = None
67    self.sdk = None
68    self.platform_minor = None
69    self.codename = None
70
71  def __repr__(self):
72    return "BuildInformation (fingerprint={}, brand={}, product={}, device={}, " \
73           "version_release={}, id={}, version_incremental={}, type={}, tags={}, " \
74           "sdk={}, platform_minor={}, codename={})"\
75      .format(self.fingerprint, self.brand, self.product, self.device, self.version_release,
76              self.id, self.version_incremental, self.type, self.tags, self.sdk,
77              self.platform_minor, self.codename)
78
79
80class ProcessCpuStats:
81  def __init__(self, command, cpu_time_ms, package_cpu_time_percent, cpu_cycles):
82    self.command = command
83    self.cpu_time_ms = cpu_time_ms
84    self.package_cpu_time_percent = package_cpu_time_percent
85    self.cpu_cycles = cpu_cycles
86
87  def __repr__(self):
88    return "ProcessCpuStats (command={}, CPU time={}ms, percent of " \
89           "package's CPU time={}%, CPU cycles={})"\
90      .format(self.command, self.cpu_time_ms, self.package_cpu_time_percent,
91              self.cpu_cycles)
92
93
94class PackageCpuStats:
95  def __init__(self, user_id, package_name, cpu_time_ms,
96      total_cpu_time_percent, cpu_cycles):
97    self.user_id = user_id
98    self.package_name = package_name
99    self.cpu_time_ms = cpu_time_ms
100    self.total_cpu_time_percent = total_cpu_time_percent
101    self.cpu_cycles = cpu_cycles
102    self.process_cpu_stats = []
103
104  def to_dict(self):
105    return {
106        "user_id": self.user_id,
107        "package_name": self.package_name,
108        "cpu_time_ms": self.cpu_time_ms,
109        "total_cpu_time_percent": self.total_cpu_time_percent,
110        "cpu_cycles": self.cpu_cycles,
111        "process_cpu_stats": [vars(p) for p in self.process_cpu_stats]
112    }
113
114  def __repr__(self):
115    process_cpu_stats_str = "[])"
116    if len(self.process_cpu_stats) > 0:
117      process_list_str = "\n      ".join(list(map(repr, self.process_cpu_stats)))
118      process_cpu_stats_str = "\n      {}\n    )".format(process_list_str)
119    return "PackageCpuStats (user id={}, package name={}, CPU time={}ms, " \
120           "percent of total CPU time={}%, CPU cycles={}, process CPU stats={}" \
121      .format(self.user_id, self.package_name, self.cpu_time_ms,
122              self.total_cpu_time_percent, self.cpu_cycles, process_cpu_stats_str)
123
124
125class StatsCollection:
126  def __init__(self):
127    self.id = -1
128    self.date = None
129    self.total_cpu_time_ms = 0
130    self.idle_cpu_time_ms = 0
131    self.io_wait_time_ms = 0
132    self.context_switches = 0
133    self.io_blocked_processes = 0
134    self.package_cpu_stats = []
135
136  def is_empty(self):
137    val = self.total_cpu_time_ms + self.idle_cpu_time_ms + self.io_wait_time_ms + \
138          self.context_switches + self.io_blocked_processes
139    return self.id == -1 and not self.date and val == 0 and len(self.package_cpu_stats) == 0
140
141  def to_dict(self):
142    return {
143        "id": self.id,
144        "date": self.date.strftime(DATETIME_FORMAT) if self.date else "",
145        "total_cpu_time_ms": self.total_cpu_time_ms,
146        "idle_cpu_time_ms": self.idle_cpu_time_ms,
147        "io_wait_time_ms": self.io_wait_time_ms,
148        "context_switches": self.context_switches,
149        "io_blocked_processes": self.io_blocked_processes,
150        "packages_cpu_stats": [p.to_dict() for p in self.package_cpu_stats]
151    }
152
153  def __repr__(self):
154    date = self.date.strftime(DATETIME_FORMAT) if self.date else ""
155    pcs_str = "\n    ".join(list(map(repr, self.package_cpu_stats)))
156    return "StatsCollection (id={}, date={}, total CPU time={}ms, " \
157           "idle CPU time={}ms, I/O wait time={}ms, total context switches={}, " \
158           "total I/O blocked processes={}, package CPU stats=\n    {}\n  )" \
159      .format(self.id, date, self.total_cpu_time_ms, self.idle_cpu_time_ms,
160              self.io_wait_time_ms, self.context_switches,
161              self.io_blocked_processes, pcs_str)
162
163
164class SystemEventStats:
165  def __init__(self):
166    self.collections = []
167
168  def add(self, collection):
169    self.collections.append(collection)
170
171  def is_empty(self):
172    return not any(map(lambda c: not c.is_empty(), self.collections))
173
174  def to_list(self):
175    return [c.to_dict() for c in self.collections]
176
177  def __repr__(self):
178    collections_str = "\n  ".join(list(map(repr, self.collections)))
179    return "SystemEventStats (\n" \
180           "  {}\n)".format(collections_str)
181
182
183class PerformanceStats:
184  def __init__(self):
185    self.boot_time_stats = None
186    self.user_switch_stats = []
187    self.custom_collection_stats = None
188
189  def has_boot_time(self):
190    return self.boot_time_stats and not self.boot_time_stats.is_empty()
191
192  def has_custom_collection(self):
193    return self.custom_collection_stats \
194           and not self.custom_collection_stats.is_empty()
195
196  def is_empty(self):
197    return not self.has_boot_time() and not self.has_custom_collection() \
198           and not any(map(lambda u: not u.is_empty(), self.user_switch_stats))
199
200  def to_dict(self):
201    return {
202        "boot_time_stats": self.boot_time_stats.to_list() if self.boot_time_stats else None,
203        "user_switch_stats": [u.to_list() for u in self.user_switch_stats],
204        "custom_collection_stats": self.custom_collection_stats.to_list() if self.custom_collection_stats else None,
205    }
206
207  def __repr__(self):
208    return "PerformanceStats (\n" \
209          "boot-time stats={}\n" \
210          "\nuser-switch stats={}\n" \
211          "\ncustom-collection stats={}\n)" \
212      .format(self.boot_time_stats, self.user_switch_stats,
213              self.custom_collection_stats)
214
215class DevicePerformanceStats:
216  def __init__(self):
217    self.build_info = None
218    self.perf_stats = []
219
220  def to_dict(self):
221    return {
222        "build_info": vars(self.build_info),
223        "perf_stats": [s.to_dict() for s in self.perf_stats]
224    }
225
226  def __repr__(self):
227    return "DevicePerformanceStats (\n" \
228            "build_info={}\n" \
229            "\nperf_stats={}\n)"\
230      .format(self.build_info, self.perf_stats)
231
232def parse_build_info(build_info_file):
233  build_info = BuildInformation()
234
235  def get_value(line):
236    if ':' not in line:
237      return ""
238    return line.split(':')[1].strip()
239
240  with open(build_info_file, 'r') as f:
241    for line in f.readlines():
242      value = get_value(line)
243      if line.startswith("fingerprint"):
244        build_info.fingerprint = value
245      elif line.startswith("brand"):
246        build_info.brand = value
247      elif line.startswith("product"):
248        build_info.product = value
249      elif line.startswith("device"):
250        build_info.device = value
251      elif line.startswith("version.release"):
252        build_info.version_release = value
253      elif line.startswith("id"):
254        build_info.id = value
255      elif line.startswith("version.incremental"):
256        build_info.version_incremental = value
257      elif line.startswith("type"):
258        build_info.type = value
259      elif line.startswith("tags"):
260        build_info.tags = value
261      elif line.startswith("sdk"):
262        build_info.sdk = value
263      elif line.startswith("platform minor version"):
264        build_info.platform_minor = value
265      elif line.startswith("codename"):
266        build_info.codename = value
267
268    return build_info
269
270def parse_cpu_times(lines, idx):
271  package_cpu_stats = []
272  package_cpu_stat = None
273
274  while not (line := lines[idx].rstrip()).startswith("Top N") \
275      and not re.match(STATS_COLLECTION_PATTERN, line) \
276      and not line.startswith('-' * 50):
277    if match := re.match(PACKAGE_CPU_STATS_PATTERN, line):
278      user_id = int(match.group(1))
279      package_name = match.group(2)
280      cpu_time_ms = int(match.group(3))
281      total_cpu_time_percent = float("{}.{}".format(match.group(4),
282                                                    match.group(5)))
283      cpu_cycles = int(match.group(7)) if match.group(7) is not None else -1
284
285      package_cpu_stat = PackageCpuStats(user_id, package_name,
286                                         cpu_time_ms,
287                                         total_cpu_time_percent,
288                                         cpu_cycles)
289      package_cpu_stats.append(package_cpu_stat)
290    elif match := re.match(PROCESS_CPU_STATS_PATTERN, line):
291      command = match.group(1)
292      cpu_time_ms = int(match.group(2))
293      package_cpu_time_percent = float("{}.{}".format(match.group(3),
294                                                      match.group(4)))
295      cpu_cycles = int(match.group(6)) if match.group(6) is not None else -1
296      if package_cpu_stat:
297        package_cpu_stat.process_cpu_stats.append(
298          ProcessCpuStats(command, cpu_time_ms, package_cpu_time_percent, cpu_cycles))
299      else:
300        print("No package CPU stats parsed for process:", command, file=sys.stderr)
301
302    idx += 1
303
304  return package_cpu_stats, idx
305
306
307def parse_collection(lines, idx, match):
308  collection = StatsCollection()
309  collection.id = int(match.group(1))
310  collection.date = datetime.strptime(match.group(2), DUMP_DATETIME_FORMAT)
311
312  while not re.match(STATS_COLLECTION_PATTERN,
313                     (line := lines[idx].strip())) and not line.startswith('-' * 50):
314    if match := re.match(TOTAL_CPU_TIME_PATTERN, line):
315      collection.total_cpu_time_ms = int(match.group(1))
316    elif match := re.match(TOTAL_IDLE_CPU_TIME_PATTERN, line):
317      collection.idle_cpu_time_ms = int(match.group(1))
318    elif match := re.match(CPU_IO_WAIT_TIME_PATTERN, line):
319      collection.io_wait_time_ms = int(match.group(1))
320    elif match := re.match(CONTEXT_SWITCHES_PATTERN, line):
321      collection.context_switches = int(match.group(1))
322    elif match := re.match(IO_BLOCKED_PROCESSES_PATTERN, line):
323      collection.io_blocked_processes = int(match.group(1))
324    elif line == TOP_N_CPU_TIME_HEADER:
325      idx += 1  # Skip subsection header
326      package_cpu_stats, idx = parse_cpu_times(lines, idx)
327      collection.package_cpu_stats = package_cpu_stats
328      continue
329
330    idx += 1
331
332  return collection, idx
333
334
335def parse_stats_collections(lines, idx):
336  system_event_stats = SystemEventStats()
337  while not (line := lines[idx].strip()).startswith('-' * 50):
338    if match := re.match(STATS_COLLECTION_PATTERN, line):
339      idx += 1  # Skip the collection header
340      collection, idx = parse_collection(lines, idx, match)
341      if not collection.is_empty():
342        system_event_stats.add(collection)
343    else:
344      idx += 1
345  return system_event_stats, idx
346
347
348def parse_dump(dump):
349  lines = dump.split("\n")
350  performance_stats = PerformanceStats()
351  idx = 0
352  while idx < len(lines):
353    line = lines[idx].strip()
354    if line == BOOT_TIME_REPORT_HEADER:
355      boot_time_stats, idx = parse_stats_collections(lines, idx)
356      if not boot_time_stats.is_empty():
357        performance_stats.boot_time_stats = boot_time_stats
358    if line == CUSTOM_COLLECTION_REPORT_HEADER:
359      idx += 2  # Skip the dashed-line after the custom collection header
360      custom_collection_stats, idx = parse_stats_collections(lines, idx)
361      if not custom_collection_stats.is_empty():
362        performance_stats.custom_collection_stats = custom_collection_stats
363    else:
364      idx += 1
365
366  return performance_stats
367
368
369def create_date_pb(date):
370  date_pb = performancestats_pb2.Date()
371  date_pb.year = date.year
372  date_pb.month = date.month
373  date_pb.day = date.day
374  return date_pb
375
376
377def create_timeofday_pb(date):
378  timeofday_pb = performancestats_pb2.TimeOfDay()
379  timeofday_pb.hours = date.hour
380  timeofday_pb.minutes = date.minute
381  timeofday_pb.seconds = date.second
382  return timeofday_pb
383
384def add_system_event_pb(system_event_stats, system_event_pb):
385  for collection in system_event_stats.collections:
386    stats_collection_pb = system_event_pb.collections.add()
387    stats_collection_pb.id = collection.id
388    stats_collection_pb.date.CopyFrom(create_date_pb(collection.date))
389    stats_collection_pb.time.CopyFrom(create_timeofday_pb(collection.date))
390    stats_collection_pb.total_cpu_time_ms = collection.total_cpu_time_ms
391    stats_collection_pb.idle_cpu_time_ms = collection.idle_cpu_time_ms
392    stats_collection_pb.io_wait_time_ms = collection.io_wait_time_ms
393    stats_collection_pb.context_switches = collection.context_switches
394    stats_collection_pb.io_blocked_processes = collection.io_blocked_processes
395
396    for package_cpu_stats in collection.package_cpu_stats:
397      package_cpu_stats_pb = stats_collection_pb.package_cpu_stats.add()
398      package_cpu_stats_pb.user_id = package_cpu_stats.user_id
399      package_cpu_stats_pb.package_name = package_cpu_stats.package_name
400      package_cpu_stats_pb.cpu_time_ms = package_cpu_stats.cpu_time_ms
401      package_cpu_stats_pb.total_cpu_time_percent = package_cpu_stats.total_cpu_time_percent
402      package_cpu_stats_pb.cpu_cycles = package_cpu_stats.cpu_cycles
403
404      for process_cpu_stats in package_cpu_stats.process_cpu_stats:
405        process_cpu_stats_pb = package_cpu_stats_pb.process_cpu_stats.add()
406        process_cpu_stats_pb.command = process_cpu_stats.command
407        process_cpu_stats_pb.cpu_time_ms = process_cpu_stats.cpu_time_ms
408        process_cpu_stats_pb.package_cpu_time_percent = process_cpu_stats.package_cpu_time_percent
409        process_cpu_stats_pb.cpu_cycles = process_cpu_stats.cpu_cycles
410
411def get_system_event(system_event_pb):
412  system_event_stats = SystemEventStats()
413  for stats_collection_pb in system_event_pb.collections:
414    stats_collection = StatsCollection()
415    stats_collection.id = stats_collection_pb.id
416    date_pb = stats_collection_pb.date
417    time_pb = stats_collection_pb.time
418    stats_collection.date = datetime(date_pb.year, date_pb.month, date_pb.day,
419                                     time_pb.hours, time_pb.minutes, time_pb.seconds)
420    stats_collection.total_cpu_time_ms = stats_collection_pb.total_cpu_time_ms
421    stats_collection.idle_cpu_time_ms = stats_collection_pb.idle_cpu_time_ms
422    stats_collection.io_wait_time_ms = stats_collection_pb.io_wait_time_ms
423    stats_collection.context_switches = stats_collection_pb.context_switches
424    stats_collection.io_blocked_processes = stats_collection_pb.io_blocked_processes
425
426    for package_cpu_stats_pb in stats_collection_pb.package_cpu_stats:
427      package_cpu_stats = \
428        PackageCpuStats(package_cpu_stats_pb.user_id,
429                        package_cpu_stats_pb.package_name,
430                        package_cpu_stats_pb.cpu_time_ms,
431                        round(package_cpu_stats_pb.total_cpu_time_percent, 2),
432                        package_cpu_stats_pb.cpu_cycles)
433
434      for process_cpu_stats_pb in package_cpu_stats_pb.process_cpu_stats:
435        process_cpu_stats = \
436          ProcessCpuStats(process_cpu_stats_pb.command,
437                          process_cpu_stats_pb.cpu_time_ms,
438                          round(process_cpu_stats_pb.package_cpu_time_percent,
439                                2),
440                          process_cpu_stats_pb.cpu_cycles)
441
442        package_cpu_stats.process_cpu_stats.append(process_cpu_stats)
443      stats_collection.package_cpu_stats.append(package_cpu_stats)
444    system_event_stats.add(stats_collection)
445
446  return system_event_stats
447
448def get_perf_stats(perf_stats_pb):
449  perf_stats = PerformanceStats()
450  perf_stats.boot_time_stats = get_system_event(perf_stats_pb.boot_time_stats)
451  perf_stats.custom_collection_stats = get_system_event(perf_stats_pb.custom_collection_stats)
452  return perf_stats
453
454def get_build_info(build_info_pb):
455  build_info = BuildInformation()
456  build_info.fingerprint = build_info_pb.fingerprint
457  build_info.brand = build_info_pb.brand
458  build_info.product = build_info_pb.product
459  build_info.device = build_info_pb.device
460  build_info.version_release = build_info_pb.version_release
461  build_info.id = build_info_pb.id
462  build_info.version_incremental = build_info_pb.version_incremental
463  build_info.type = build_info_pb.type
464  build_info.tags = build_info_pb.tags
465  build_info.sdk = build_info_pb.sdk
466  build_info.platform_minor = build_info_pb.platform_minor
467  build_info.codename = build_info_pb.codename
468  return build_info
469
470def write_pb(perf_stats, out_file, build_info=None, out_build_file=None):
471  if perf_stats.is_empty():
472    print("Cannot write proto since performance stats are empty")
473    return False
474
475  perf_stats_pb = performancestats_pb2.PerformanceStats()
476
477  # Boot time proto
478  if perf_stats.has_boot_time():
479    boot_time_stats_pb = performancestats_pb2.SystemEventStats()
480    add_system_event_pb(perf_stats.boot_time_stats, boot_time_stats_pb)
481    perf_stats_pb.boot_time_stats.CopyFrom(boot_time_stats_pb)
482
483  # TODO(b/256654082): Add user switch events to proto
484
485  # Custom collection proto
486  if perf_stats.has_custom_collection():
487    custom_collection_stats_pb = performancestats_pb2.SystemEventStats()
488    add_system_event_pb(perf_stats.custom_collection_stats,
489                        custom_collection_stats_pb)
490    perf_stats_pb.custom_collection_stats.CopyFrom(custom_collection_stats_pb)
491
492  # Write pb binary to disk
493  if out_file:
494    with open(out_file, "wb") as f:
495      f.write(perf_stats_pb.SerializeToString())
496
497  if build_info is not None:
498    build_info_pb = deviceperformancestats_pb2.BuildInformation()
499    build_info_pb.fingerprint = build_info.fingerprint
500    build_info_pb.brand = build_info.brand
501    build_info_pb.product = build_info.product
502    build_info_pb.device = build_info.device
503    build_info_pb.version_release = build_info.version_release
504    build_info_pb.id = build_info.id
505    build_info_pb.version_incremental = build_info.version_incremental
506    build_info_pb.type = build_info.type
507    build_info_pb.tags = build_info.tags
508    build_info_pb.sdk = build_info.sdk
509    build_info_pb.platform_minor = build_info.platform_minor
510    build_info_pb.codename = build_info.codename
511
512    device_run_perf_stats_pb = deviceperformancestats_pb2.DevicePerformanceStats()
513    device_run_perf_stats_pb.build_info.CopyFrom(build_info_pb)
514    device_run_perf_stats_pb.perf_stats.add().CopyFrom(perf_stats_pb)
515
516    with open(out_build_file, "wb") as f:
517      f.write(device_run_perf_stats_pb.SerializeToString())
518
519  return True
520
521
522def read_pb(pb_file, is_device_run=False):
523  perf_stats_pb = deviceperformancestats_pb2.DevicePerformanceStats() if \
524    is_device_run else performancestats_pb2.PerformanceStats()
525
526  with open(pb_file, "rb") as f:
527    try:
528      perf_stats_pb.ParseFromString(f.read())
529      perf_stats_pb.DiscardUnknownFields()
530    except UnicodeDecodeError:
531      proto_type = "DevicePerformanceStats" if is_device_run else "PerformanceStats"
532      print(f"Error: Proto in {pb_file} probably is not '{proto_type}'")
533      return None
534
535  if not perf_stats_pb:
536    print(f"Error: Proto stored in {pb_file} has incorrect format.")
537    return None
538
539  if not is_device_run:
540    return get_perf_stats(perf_stats_pb)
541
542  device_run_perf_stats = DevicePerformanceStats()
543  device_run_perf_stats.build_info = get_build_info(perf_stats_pb.build_info)
544
545  for perf_stat in perf_stats_pb.perf_stats:
546    device_run_perf_stats.perf_stats.append(get_perf_stats(perf_stat))
547
548  return device_run_perf_stats
549
550def init_arguments():
551  parser = argparse.ArgumentParser(description="Parses CarWatchdog's dump.")
552  parser.add_argument("-f", "--file", dest="file",
553                      default="dump.txt",
554                      help="File with the CarWatchdog dump")
555  parser.add_argument("-o", "--out", dest="out",
556                      help="protobuf binary with parsed performance stats")
557  parser.add_argument("-b", "--build", dest="build",
558                      help="File with Android device build information")
559  parser.add_argument("-d", "--device-out", dest="device_out",
560                      default="device_perf_stats.pb",
561                      help="protobuf binary with build information")
562  parser.add_argument("-p", "--print", dest="print", action="store_true",
563                      help="prints the parsed performance data to the console "
564                           "when out proto defined")
565  parser.add_argument("-r", "--read", dest="read_proto",
566                      help="Protobuf binary to be printed in console. If this "
567                           "flag is set no other process is executed.")
568  parser.add_argument("-D", "--device-run", dest="device_run",
569                      action="store_true",
570                      help="Specifies that the proto to be read is a "
571                           "DevicePerformanceStats proto. (Only checked if "
572                           "-r is set)")
573  parser.add_argument("-j", "--json", dest="json",
574                      action="store_true",
575                      help="Generate a JSON file from the protobuf binary read.")
576
577  return parser.parse_args()
578
579if __name__ == "__main__":
580  args = init_arguments()
581
582  if args.read_proto:
583    if not os.path.isfile(args.read_proto):
584      print("Error: Proto binary '%s' does not exist" % args.read_proto)
585      sys.exit(1)
586    performance_stats = read_pb(args.read_proto, args.device_run)
587    if performance_stats is None:
588      print(f"Error: Could not read '{args.read_proto}'")
589      sys.exit(1)
590    if args.json:
591      print(json.dumps(performance_stats.to_dict()))
592    else:
593      print("Reading performance stats proto:")
594      print(performance_stats)
595    sys.exit()
596
597  if not os.path.isfile(args.file):
598    print("Error: File '%s' does not exist" % args.file)
599    sys.exit(1)
600
601  with open(args.file, 'r', encoding="UTF-8", errors="ignore") as f:
602    performance_stats = parse_dump(f.read())
603
604    build_info = None
605    if args.build:
606      build_info = parse_build_info(args.build)
607      print(build_info)
608
609    if performance_stats.is_empty():
610      print("Error: No performance stats were parsed. Make sure dump file contains carwatchdog's "
611            "dump text.")
612      sys.exit(1)
613
614    if (args.out or args.build) and write_pb(performance_stats, args.out,
615                                             build_info, args.device_out):
616      out_file = args.out if args.out else args.device_out
617      print("Output protobuf binary in:", out_file)
618
619    if args.print or not (args.out or args.build):
620      if args.json:
621          print(json.dumps(performance_stats.to_dict()))
622          sys.exit()
623      print(performance_stats)
624