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"""Parser library to parse CarWatchdog dumpsys string.""" 18 19import datetime 20import json 21import re 22import sys 23from typing import Any, Dict, List, Optional, Tuple 24from . import performancestats_pb2 25 26 27BOOT_TIME_REPORT_HEADER_PATTERN = ( 28 r"Boot-time (?:performance|collection) report:" 29) 30TOP_N_STORAGE_IO_READS_HEADER_PATTERN = r"Top N (?:Storage I/O )?Reads:" 31TOP_N_STORAGE_IO_WRITES_HEADER_PATTERN = r"Top N (?:Storage I/O )?Writes:" 32STATS_COLLECTION_PATTERN = r"Collection (?P<id>\d+): <(?P<date>.+)>" 33PACKAGE_STORAGE_IO_STATS_PATTERN = ( 34 r"(?P<userId>\d+), (?P<packageName>.+), (?P<fgBytes>\d+)," 35 r" (?P<fgBytesPercent>\d+.\d+)%, " 36 r"(?P<fgFsync>\d+), (?P<fgFsyncPercent>\d+.\d+)%, (?P<bgBytes>\d+), " 37 r"(?P<bgBytesPercent>\d+.\d+)%, (?P<bgFsync>\d+)," 38 r" (?P<bgFsyncPercent>\d+.\d+)%" 39) 40PACKAGE_CPU_STATS_PATTERN = ( 41 r"(?P<userId>\d+), (?P<packageName>.+), (?P<cpuTimeMs>\d+)," 42 r" (?P<cpuTimePercent>\d+\.\d+)%" 43 r"(, (?P<cpuCycles>\d+))?" 44) 45PROCESS_CPU_STATS_PATTERN = ( 46 r"\s+(?P<command>.+), (?P<cpuTimeMs>\d+), (?P<uidCpuPercent>\d+.\d+)%" 47 r"(, (?P<cpuCycles>\d+))?" 48) 49TOTAL_CPU_TIME_PATTERN = r"Total CPU time \\(ms\\): (?P<totalCpuTimeMs>\d+)" 50TOTAL_CPU_CYCLES_PATTERN = r"Total CPU cycles: (?P<totalCpuCycles>\d+)" 51TOTAL_IDLE_CPU_TIME_PATTERN = ( 52 r"Total idle CPU time \\(ms\\)/percent: (?P<idleCpuTimeMs>\d+) / .+" 53) 54CPU_IO_WAIT_TIME_PATTERN = ( 55 r"CPU I/O wait time(?: \\(ms\\))?/percent: (?P<iowaitCpuTimeMs>\d+) / .+" 56) 57CONTEXT_SWITCHES_PATTERN = ( 58 r"Number of context switches: (?P<totalCtxtSwitches>\d+)" 59) 60IO_BLOCKED_PROCESSES_PATTERN = ( 61 r"Number of I/O blocked processes/percent: (?P<totalIoBlkProc>\d+)" r" / .+" 62) 63MAJOR_PAGE_FAULTS_PATTERN = ( 64 r"Number of major page faults since last collection:" 65 r" (?P<totalMajPgFaults>\d+)" 66) 67 68COLLECTION_END_LINE_MIN_LEN = 50 69PERIODIC_COLLECTION_HEADER = "Periodic collection report:" 70LAST_N_MINS_COLLECTION_HEADER = "Last N minutes performance report:" 71CUSTOM_COLLECTION_REPORT_HEADER = "Custom performance data report:" 72TOP_N_CPU_TIME_HEADER = "Top N CPU Times:" 73 74DUMP_DATETIME_FORMAT = "%a %b %d %H:%M:%S %Y %Z" 75DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" 76 77 78class BuildInformation: 79 """Contains Android build information.""" 80 81 def __init__(self): 82 self.fingerprint = None 83 self.brand = None 84 self.product = None 85 self.device = None 86 self.version_release = None 87 self.id = None 88 self.version_incremental = None 89 self.type = None 90 self.tags = None 91 self.sdk = None 92 self.platform_minor = None 93 self.codename = None 94 95 def __repr__(self) -> str: 96 return ( 97 "BuildInformation (fingerprint={}, brand={}, product={}, device={}, " 98 "version_release={}, id={}, version_incremental={}, type={}, tags={}, " 99 "sdk={}, platform_minor={}, codename={})".format( 100 self.fingerprint, 101 self.brand, 102 self.product, 103 self.device, 104 self.version_release, 105 self.id, 106 self.version_incremental, 107 self.type, 108 self.tags, 109 self.sdk, 110 self.platform_minor, 111 self.codename, 112 ) 113 ) 114 115 116class ProcessCpuStats: 117 """Contains the CPU stats for a top process in a package.""" 118 119 def __init__( 120 self, command, cpu_time_ms, package_cpu_time_percent, cpu_cycles 121 ): 122 self.command = command 123 self.cpu_time_ms = cpu_time_ms 124 self.package_cpu_time_percent = package_cpu_time_percent 125 self.cpu_cycles = cpu_cycles 126 127 @classmethod 128 def from_proto( 129 cls, stats_pb: performancestats_pb2.ProcessCpuStats 130 ) -> "ProcessCpuStats": 131 """Generates ProcessCpuStats instance from the proto object.""" 132 return cls( 133 stats_pb.command, 134 stats_pb.cpu_time_ms, 135 round(stats_pb.package_cpu_time_percent, 2), 136 stats_pb.cpu_cycles, 137 ) 138 139 def __repr__(self) -> str: 140 return ( 141 "ProcessCpuStats (command={}, CPU time={}ms, percent of " 142 "package's CPU time={}%, CPU cycles={})".format( 143 self.command, 144 self.cpu_time_ms, 145 self.package_cpu_time_percent, 146 self.cpu_cycles, 147 ) 148 ) 149 150 151class PackageCpuStats: 152 """Contains the CPU stats for a top package.""" 153 154 def __init__( 155 self, 156 user_id, 157 package_name, 158 cpu_time_ms, 159 total_cpu_time_percent, 160 cpu_cycles, 161 ): 162 self.user_id = user_id 163 self.package_name = package_name 164 self.cpu_time_ms = cpu_time_ms 165 self.total_cpu_time_percent = total_cpu_time_percent 166 self.cpu_cycles = cpu_cycles 167 self.process_cpu_stats = [] 168 169 @classmethod 170 def from_proto( 171 cls, stats_pb: performancestats_pb2.PackageCpuStats 172 ) -> "PackageCpuStats": 173 """Generates PackageCpuStats instance from the proto object.""" 174 return cls( 175 stats_pb.user_id, 176 stats_pb.package_name, 177 stats_pb.cpu_time_ms, 178 round(stats_pb.total_cpu_time_percent, 2), 179 stats_pb.cpu_cycles, 180 ) 181 182 def to_dict(self) -> Dict[str, Any]: 183 """Generates a dictionary equivalent where the field names are the keys.""" 184 return { 185 "user_id": self.user_id, 186 "package_name": self.package_name, 187 "cpu_time_ms": self.cpu_time_ms, 188 "total_cpu_time_percent": self.total_cpu_time_percent, 189 "cpu_cycles": self.cpu_cycles, 190 "process_cpu_stats": [vars(p) for p in self.process_cpu_stats], 191 } 192 193 def __repr__(self) -> str: 194 process_cpu_stats_str = "[])" 195 if self.process_cpu_stats: 196 process_list_str = "\n ".join( 197 list(map(repr, self.process_cpu_stats)) 198 ) 199 process_cpu_stats_str = "\n {}\n )".format(process_list_str) 200 return ( 201 "PackageCpuStats (user id={}, package name={}, CPU time={}ms, " 202 "percent of total CPU time={}%, CPU cycles={}, process CPU stats={})" 203 .format( 204 self.user_id, 205 self.package_name, 206 self.cpu_time_ms, 207 self.total_cpu_time_percent, 208 self.cpu_cycles, 209 process_cpu_stats_str, 210 ) 211 ) 212 213 214class PackageStorageIoStats: 215 """Contains a package's storage I/O read/write stats.""" 216 217 def __init__( 218 self, 219 user_id, 220 package_name, 221 fg_bytes, 222 fg_bytes_percent, 223 fg_fsync, 224 fg_fsync_percent, 225 bg_bytes, 226 bg_bytes_percent, 227 bg_fsync, 228 bg_fsync_percent, 229 ): 230 self.user_id = user_id 231 self.package_name = package_name 232 self.fg_bytes = fg_bytes 233 self.fg_bytes_percent = fg_bytes_percent 234 self.fg_fsync = fg_fsync 235 self.fg_fsync_percent = fg_fsync_percent 236 self.bg_bytes = bg_bytes 237 self.bg_bytes_percent = bg_bytes_percent 238 self.bg_fsync = bg_fsync 239 self.bg_fsync_percent = bg_fsync_percent 240 241 @classmethod 242 def from_proto( 243 cls, stats_pb: performancestats_pb2.PackageStorageIoStats 244 ) -> "PackageStorageIoStats": 245 """Generates PackageStorageIoStats instance from the proto object.""" 246 return cls( 247 stats_pb.user_id, 248 stats_pb.package_name, 249 stats_pb.fg_bytes, 250 round(stats_pb.fg_bytes_percent, 2), 251 stats_pb.fg_fsync, 252 round(stats_pb.fg_fsync_percent, 2), 253 stats_pb.bg_bytes, 254 round(stats_pb.bg_bytes_percent, 2), 255 stats_pb.bg_fsync, 256 round(stats_pb.bg_fsync_percent, 2), 257 ) 258 259 def to_dict(self) -> Dict[str, Any]: 260 """Generates a dictionary equivalent where the field names are the keys.""" 261 return { 262 "user_id": self.user_id, 263 "package_name": self.package_name, 264 "fg_bytes": self.fg_bytes, 265 "fg_bytes_percent": self.fg_bytes_percent, 266 "fg_fsync": self.fg_fsync, 267 "fg_fsync_percent": self.fg_fsync_percent, 268 "bg_bytes": self.bg_bytes, 269 "bg_bytes_percent": self.bg_bytes_percent, 270 "bg_fsync": self.bg_fsync, 271 "bg_fsync_percent": self.bg_fsync_percent, 272 } 273 274 def __repr__(self) -> str: 275 return ( 276 "PackageStorageIoStats (user id={}, package name={}, foreground" 277 " bytes={}, foreground bytes percent={}, foreground fsync={}," 278 " foreground fsync percent={}, background bytes={}, background bytes" 279 " percent={}, background fsync={}, background fsync percent={}) " 280 .format( 281 self.user_id, 282 self.package_name, 283 self.fg_bytes, 284 self.fg_bytes_percent, 285 self.fg_fsync, 286 self.fg_fsync_percent, 287 self.bg_bytes, 288 self.bg_bytes_percent, 289 self.bg_fsync, 290 self.bg_fsync_percent, 291 ) 292 ) 293 294 295class StatsCollection: 296 """Contains stats recorded during a single collection polling.""" 297 298 def __init__(self): 299 self.id = -1 300 self.date = None 301 self.total_cpu_time_ms = 0 302 self.total_cpu_cycles = 0 303 self.idle_cpu_time_ms = 0 304 self.io_wait_time_ms = 0 305 self.context_switches = 0 306 self.io_blocked_processes = 0 307 self.major_page_faults = 0 308 self.package_cpu_stats = [] 309 self.package_storage_io_read_stats = [] 310 self.package_storage_io_write_stats = [] 311 312 def is_empty(self) -> bool: 313 """Returns true when the object is empty.""" 314 val = ( 315 self.total_cpu_time_ms 316 + self.total_cpu_cycles 317 + self.idle_cpu_time_ms 318 + self.io_wait_time_ms 319 + self.context_switches 320 + self.io_blocked_processes 321 + self.major_page_faults 322 ) 323 return ( 324 self.id == -1 325 and not self.date 326 and val == 0 327 and not self.package_cpu_stats 328 and not self.package_storage_io_read_stats 329 and not self.package_storage_io_write_stats 330 ) 331 332 def to_dict(self) -> Dict[str, Any]: 333 """Generates a dictionary equivalent where the field names are the keys.""" 334 return { 335 "id": self.id, 336 "date": self.date.strftime(DATETIME_FORMAT) if self.date else "", 337 "total_cpu_time_ms": self.total_cpu_time_ms, 338 "total_cpu_cycles": self.total_cpu_cycles, 339 "idle_cpu_time_ms": self.idle_cpu_time_ms, 340 "io_wait_time_ms": self.io_wait_time_ms, 341 "context_switches": self.context_switches, 342 "io_blocked_processes": self.io_blocked_processes, 343 "major_page_faults": self.major_page_faults, 344 "package_cpu_stats": [p.to_dict() for p in self.package_cpu_stats], 345 "package_storage_io_read_stats": [ 346 p.to_dict() for p in self.package_storage_io_read_stats 347 ], 348 "package_storage_io_write_stats": [ 349 p.to_dict() for p in self.package_storage_io_write_stats 350 ], 351 } 352 353 def __repr__(self) -> str: 354 date = self.date.strftime(DATETIME_FORMAT) if self.date else "" 355 package_cpu_stats_dump = "" 356 package_storage_io_read_stats_dump = "" 357 package_storage_io_write_stats_dump = "" 358 359 if self.package_cpu_stats: 360 package_cpu_stats_str = "\n ".join( 361 list(map(repr, self.package_cpu_stats)) 362 ) 363 package_cpu_stats_dump = ", package CPU stats=\n {}\n".format( 364 package_cpu_stats_str 365 ) 366 367 if self.package_storage_io_read_stats: 368 package_storage_io_read_stats_str = "\n ".join( 369 list(map(repr, self.package_storage_io_read_stats)) 370 ) 371 package_storage_io_read_stats_dump = ( 372 ", package storage I/O read stats=\n {}\n".format( 373 package_storage_io_read_stats_str 374 ) 375 ) 376 377 if self.package_storage_io_write_stats: 378 package_storage_io_write_stats_str = "\n ".join( 379 list(map(repr, self.package_storage_io_write_stats)) 380 ) 381 package_storage_io_write_stats_dump = ( 382 ", package storage I/O write stats=\n {}\n".format( 383 package_storage_io_write_stats_str 384 ) 385 ) 386 387 return ( 388 "StatsCollection (id={}, date={}, total CPU time={}ms, total CPU" 389 " cycles={}, idle CPU time={}ms, I/O wait time={}ms, total context" 390 " switches={}, total I/O blocked processes={}, major page" 391 " faults={}{}{}{})\n".format( 392 self.id, 393 date, 394 self.total_cpu_time_ms, 395 self.total_cpu_cycles, 396 self.idle_cpu_time_ms, 397 self.io_wait_time_ms, 398 self.context_switches, 399 self.io_blocked_processes, 400 self.major_page_faults, 401 package_cpu_stats_dump, 402 package_storage_io_read_stats_dump, 403 package_storage_io_write_stats_dump, 404 ) 405 ) 406 407 408class SystemEventStats: 409 """Contains stats recorded from all pollings during a system event.""" 410 411 def __init__(self): 412 self.collections = [] 413 414 def add(self, collection: StatsCollection) -> None: 415 """Adds the collection stats to the system event.""" 416 self.collections.append(collection) 417 418 def is_empty(self) -> bool: 419 """Returns true when the object is empty.""" 420 return not any(map(lambda c: not c.is_empty(), self.collections)) 421 422 def to_list(self) -> List[Dict[str, Any]]: 423 """Generates a list equivalent of the object.""" 424 return [c.to_dict() for c in self.collections] 425 426 def __repr__(self) -> str: 427 collections_str = "\n ".join(list(map(repr, self.collections))) 428 return "SystemEventStats (\n {}\n)".format(collections_str) 429 430 431class PerformanceStats: 432 """Contains CarWatchdog stats captured in a dumpsys output.""" 433 434 def __init__(self): 435 self._boot_time_stats = None 436 self._last_n_minutes_stats = None 437 self._user_switch_stats = [] 438 self._custom_collection_stats = None 439 440 def _has_boot_time_stats(self) -> bool: 441 """Returns true when boot_time_stats are available.""" 442 return self._boot_time_stats and not self._boot_time_stats.is_empty() 443 444 def _has_last_n_minutes_stats(self) -> bool: 445 """Returns true when last_n_minutes_stats are available.""" 446 return ( 447 self._last_n_minutes_stats and not self._last_n_minutes_stats.is_empty() 448 ) 449 450 def _has_custom_collection_stats(self) -> bool: 451 """Returns true when custom_collection_stats are available.""" 452 return ( 453 self._custom_collection_stats 454 and not self._custom_collection_stats.is_empty() 455 ) 456 457 def set_boot_time_stats(self, stats: SystemEventStats) -> None: 458 """Sets the boot time stats.""" 459 self._boot_time_stats = stats 460 461 def set_last_n_minutes_stats(self, stats: SystemEventStats) -> None: 462 """Sets the last n minutes stats.""" 463 self._last_n_minutes_stats = stats 464 465 def set_custom_collection_stats(self, stats: SystemEventStats) -> None: 466 """Sets the custom collection stats.""" 467 self._custom_collection_stats = stats.collections 468 469 def get_boot_time_stats(self) -> Optional[SystemEventStats]: 470 """Returns the boot time stats.""" 471 if self._has_boot_time_stats(): 472 return self._boot_time_stats 473 return None 474 475 def get_last_n_minutes_stats(self) -> Optional[SystemEventStats]: 476 """Returns the last n minutes stats.""" 477 if self._has_last_n_minutes_stats(): 478 return self._last_n_minutes_stats 479 return None 480 481 def get_custom_collection_stats(self) -> Optional[SystemEventStats]: 482 """Returns the custom collection stats.""" 483 if self._has_custom_collection_stats(): 484 return self._custom_collection_stats 485 return None 486 487 def is_empty(self) -> bool: 488 """Return true when the object is empty.""" 489 return ( 490 not self._has_boot_time_stats() 491 and not self._has_last_n_minutes_stats() 492 and not self._has_custom_collection_stats() 493 and not any(map(lambda u: not u.is_empty(), self._user_switch_stats)) 494 ) 495 496 def to_dict(self) -> Optional[Dict[str, Any]]: 497 """Generates a dictionary equivalent where the field names are the keys.""" 498 return { 499 "boot_time_stats": ( 500 self._boot_time_stats.to_list() if self._boot_time_stats else None 501 ), 502 "last_n_minutes_stats": ( 503 self._last_n_minutes_stats.to_list() 504 if self._last_n_minutes_stats 505 else None 506 ), 507 "user_switch_stats": [u.to_list() for u in self._user_switch_stats], 508 "custom_collection_stats": ( 509 self._custom_collection_stats.to_list() 510 if self._custom_collection_stats 511 else None 512 ), 513 } 514 515 def __repr__(self) -> str: 516 return ( 517 "PerformanceStats (\n" 518 "boot-time stats={}\n" 519 "\nlast n minutes stats={}\n" 520 "\nuser-switch stats={}\n" 521 "\ncustom-collection stats={}\n)".format( 522 self._boot_time_stats, 523 self._last_n_minutes_stats, 524 self._user_switch_stats, 525 self._custom_collection_stats, 526 ) 527 ) 528 529 530class DevicePerformanceStats: 531 """Contains the build information and the CarWatchdog stats.""" 532 533 def __init__(self): 534 self.build_info = None 535 self.perf_stats = [] 536 537 def to_dict(self) -> Dict[str, Any]: 538 """Generates a dictionary equivalent where the field names are the keys.""" 539 return { 540 "build_info": vars(self.build_info), 541 "perf_stats": [s.to_dict() for s in self.perf_stats], 542 } 543 544 def __repr__(self) -> str: 545 return "DevicePerformanceStats (\nbuild_info={}\n\nperf_stats={}\n)".format( 546 self.build_info, self.perf_stats 547 ) 548 549 550def parse_build_info(build_info_file: str) -> BuildInformation: 551 """Parses and returns the BuildInformation from the build info file.""" 552 build_info = BuildInformation() 553 554 def get_value(line): 555 if ":" not in line: 556 return "" 557 return line.split(":")[1].strip() 558 559 with open(build_info_file, "r") as f: 560 for line in f: 561 value = get_value(line) 562 if line.startswith("fingerprint"): 563 build_info.fingerprint = value 564 elif line.startswith("brand"): 565 build_info.brand = value 566 elif line.startswith("product"): 567 build_info.product = value 568 elif line.startswith("device"): 569 build_info.device = value 570 elif line.startswith("version.release"): 571 build_info.version_release = value 572 elif line.startswith("id"): 573 build_info.id = value 574 elif line.startswith("version.incremental"): 575 build_info.version_incremental = value 576 elif line.startswith("type"): 577 build_info.type = value 578 elif line.startswith("tags"): 579 build_info.tags = value 580 elif line.startswith("sdk"): 581 build_info.sdk = value 582 elif line.startswith("platform minor version"): 583 build_info.platform_minor = value 584 elif line.startswith("codename"): 585 build_info.codename = value 586 587 return build_info 588 589 590def _is_stats_section_end(line: str) -> bool: 591 return ( 592 line.startswith("Top N") 593 or re.fullmatch(STATS_COLLECTION_PATTERN, line) is not None 594 or line.startswith("-" * COLLECTION_END_LINE_MIN_LEN) 595 ) 596 597 598def _parse_cpu_stats( 599 lines: List[str], idx: int 600) -> Tuple[List[PackageCpuStats], int]: 601 """Parses the CPU stats from the lines.""" 602 package_cpu_stats = [] 603 package_cpu_stat = None 604 605 while not _is_stats_section_end(line := lines[idx].rstrip()): 606 if match := re.fullmatch(PACKAGE_CPU_STATS_PATTERN, line): 607 cpu_cycles_str = match.group("cpuCycles") 608 609 package_cpu_stat = PackageCpuStats( 610 int(match.group("userId")), 611 match.group("packageName"), 612 int(match.group("cpuTimeMs")), 613 float(match.group("cpuTimePercent")), 614 int(cpu_cycles_str) if cpu_cycles_str is not None else -1, 615 ) 616 package_cpu_stats.append(package_cpu_stat) 617 elif match := re.fullmatch(PROCESS_CPU_STATS_PATTERN, line): 618 command = match.group("command") 619 cpu_cycles_str = match.group("cpuCycles") 620 if package_cpu_stat: 621 package_cpu_stat.process_cpu_stats.append( 622 ProcessCpuStats( 623 command, 624 int(match.group("cpuTimeMs")), 625 float(match.group("uidCpuPercent")), 626 int(cpu_cycles_str) if cpu_cycles_str is not None else -1, 627 ) 628 ) 629 else: 630 print( 631 "No package CPU stats parsed for process:", command, file=sys.stderr 632 ) 633 634 idx += 1 635 636 return package_cpu_stats, idx 637 638 639def _parse_storage_io_stats( 640 lines: List[str], idx: int 641) -> Tuple[List[PackageStorageIoStats], int]: 642 """Parses the storage I/O stats from the lines.""" 643 package_storage_io_stats = [] 644 645 while not _is_stats_section_end(line := lines[idx].rstrip()): 646 if match := re.fullmatch(PACKAGE_STORAGE_IO_STATS_PATTERN, line): 647 package_storage_io_stats.append( 648 PackageStorageIoStats( 649 int(match.group("userId")), 650 match.group("packageName"), 651 int(match.group("fgBytes")), 652 float(match.group("fgBytesPercent")), 653 int(match.group("fgFsync")), 654 float(match.group("fgFsyncPercent")), 655 int(match.group("bgBytes")), 656 float(match.group("bgBytesPercent")), 657 int(match.group("bgFsync")), 658 float(match.group("bgFsyncPercent")), 659 ) 660 ) 661 662 idx += 1 663 664 return package_storage_io_stats, idx 665 666 667def _parse_collection( 668 lines: List[str], idx: int, match: re.Match[str] 669) -> Tuple[StatsCollection, int]: 670 """Parses the stats recorded for a single polling.""" 671 collection = StatsCollection() 672 collection.id = int(match.group("id")) 673 collection.date = datetime.datetime.strptime( 674 match.group("date"), DUMP_DATETIME_FORMAT 675 ) 676 677 while not ( 678 re.fullmatch(STATS_COLLECTION_PATTERN, (line := lines[idx].strip())) 679 or line.startswith("-" * COLLECTION_END_LINE_MIN_LEN) 680 ): 681 if match := re.fullmatch(TOTAL_CPU_TIME_PATTERN, line): 682 collection.total_cpu_time_ms = int(match.group("totalCpuTimeMs")) 683 if match := re.fullmatch(TOTAL_CPU_CYCLES_PATTERN, line): 684 collection.total_cycles = int(match.group("totalCpuCycles")) 685 elif match := re.fullmatch(TOTAL_IDLE_CPU_TIME_PATTERN, line): 686 collection.idle_cpu_time_ms = int(match.group("idleCpuTimeMs")) 687 elif match := re.fullmatch(CPU_IO_WAIT_TIME_PATTERN, line): 688 collection.io_wait_time_ms = int(match.group("iowaitCpuTimeMs")) 689 elif match := re.fullmatch(CONTEXT_SWITCHES_PATTERN, line): 690 collection.context_switches = int(match.group("totalCtxtSwitches")) 691 elif match := re.fullmatch(IO_BLOCKED_PROCESSES_PATTERN, line): 692 collection.io_blocked_processes = int(match.group("totalIoBlkProc")) 693 elif match := re.fullmatch(MAJOR_PAGE_FAULTS_PATTERN, line): 694 collection.major_page_faults = int(match.group("totalMajPgFaults")) 695 elif line == TOP_N_CPU_TIME_HEADER: 696 idx += 1 # Skip subsection header 697 package_cpu_stats, idx = _parse_cpu_stats(lines, idx) 698 collection.package_cpu_stats = package_cpu_stats 699 continue 700 elif re.fullmatch(TOP_N_STORAGE_IO_READS_HEADER_PATTERN, line): 701 idx += 1 702 package_storage_io_stats, idx = _parse_storage_io_stats(lines, idx) 703 collection.package_storage_io_read_stats = package_storage_io_stats 704 continue 705 elif re.fullmatch(TOP_N_STORAGE_IO_WRITES_HEADER_PATTERN, line): 706 idx += 1 707 package_storage_io_stats, idx = _parse_storage_io_stats(lines, idx) 708 collection.package_storage_io_write_stats = package_storage_io_stats 709 continue 710 idx += 1 711 712 return collection, idx 713 714 715def _parse_stats_collections( 716 lines: List[str], idx: int 717) -> Tuple[SystemEventStats, int]: 718 """Parses the stats recorded for a system event.""" 719 system_event_stats = SystemEventStats() 720 while not (line := lines[idx].strip()).startswith("-" * 50): 721 if match := re.fullmatch(STATS_COLLECTION_PATTERN, line): 722 idx += 1 # Skip the collection header 723 collection, idx = _parse_collection(lines, idx, match) 724 if not collection.is_empty(): 725 system_event_stats.add(collection) 726 else: 727 idx += 1 728 return system_event_stats, idx 729 730 731def parse_dump(dump: str) -> PerformanceStats: 732 """Parses/returns a PerformanceStats object from CarWatchdog dump string.""" 733 lines = dump.split("\n") 734 performance_stats = PerformanceStats() 735 idx = 0 736 while idx < len(lines): 737 line = lines[idx].strip() 738 if re.fullmatch(BOOT_TIME_REPORT_HEADER_PATTERN, line): 739 boot_time_stats, idx = _parse_stats_collections(lines, idx) 740 if not boot_time_stats.is_empty(): 741 performance_stats.set_boot_time_stats(boot_time_stats) 742 if ( 743 line == PERIODIC_COLLECTION_HEADER 744 or line == LAST_N_MINS_COLLECTION_HEADER 745 ): 746 last_n_minutes_stats, idx = _parse_stats_collections(lines, idx) 747 if not last_n_minutes_stats.is_empty(): 748 performance_stats.set_last_n_minutes_stats(last_n_minutes_stats) 749 if line == CUSTOM_COLLECTION_REPORT_HEADER: 750 idx += 2 # Skip the dashed-line after the custom collection header 751 custom_collection_stats, idx = _parse_stats_collections(lines, idx) 752 if not custom_collection_stats.is_empty(): 753 performance_stats.set_custom_collection_stats(custom_collection_stats) 754 else: 755 idx += 1 756 757 return performance_stats 758 759 760def parse_dump_to_json(dump: str) -> Any: 761 """Parses and returns a json object from the CarWatchdog dump string.""" 762 return json.loads(json.dumps(parse_dump(dump).to_dict())) 763