|
6 | 6 | import os |
7 | 7 | import glob |
8 | 8 | import re |
| 9 | +import tempfile |
9 | 10 | import time |
10 | 11 | import json |
11 | 12 |
|
@@ -767,6 +768,43 @@ def run(self): |
767 | 768 | RunCommand(installCmd, verbose=True).run() |
768 | 769 | getLogger().info("Completed install.") |
769 | 770 |
|
| 771 | + # The Mac.iPhone.17.Perf devices in the Helix pool are shared across many test |
| 772 | + # runs over days, and iOS retains crash reports in /var/mobile/Library/Logs/ |
| 773 | + # CrashReporter/ until rotated out. To avoid re-uploading that historical |
| 774 | + # backlog on a kill failure, snapshot the device's current crash report set |
| 775 | + # now and below upload only reports that appeared since this snapshot. |
| 776 | + # This mirrors XHarness's CrashSnapshotReporter pattern (it's a copy, not a |
| 777 | + # move — device state is unchanged). |
| 778 | + def listDeviceCrashReports(): |
| 779 | + """Return the set of crash report identifiers currently on the device, |
| 780 | + or None if listing failed.""" |
| 781 | + listFilePath = None |
| 782 | + try: |
| 783 | + with tempfile.NamedTemporaryFile(mode='w', suffix='.list', delete=False) as listFile: |
| 784 | + listFilePath = listFile.name |
| 785 | + listCmd = xharnesscommand() + [ |
| 786 | + 'apple', 'mlaunch', '--', |
| 787 | + f'--list-crash-reports={listFilePath}', |
| 788 | + '--devname', deviceUDID, |
| 789 | + ] |
| 790 | + RunCommand(listCmd, verbose=True).run() |
| 791 | + with open(listFilePath) as f: |
| 792 | + return {line.strip() for line in f if line.strip()} |
| 793 | + except Exception as listEx: |
| 794 | + getLogger().warning(f"Failed to list device crash reports: {listEx}") |
| 795 | + return None |
| 796 | + finally: |
| 797 | + if listFilePath and os.path.exists(listFilePath): |
| 798 | + try: |
| 799 | + os.remove(listFilePath) |
| 800 | + except OSError: |
| 801 | + pass |
| 802 | + |
| 803 | + getLogger().info("Snapshotting existing crash reports on device.") |
| 804 | + initial_device_crashes = listDeviceCrashReports() |
| 805 | + if initial_device_crashes is not None: |
| 806 | + getLogger().info(f"Found {len(initial_device_crashes)} pre-existing crash report(s).") |
| 807 | + |
770 | 808 | allResults = [] |
771 | 809 | timeToFirstDrawEventEndDateTime = datetime.now() + timedelta(minutes=-10) # This is used to keep track of the latest time to draw end event, we use this to calculate time to draw and also as a reference point for the next iteration log time. |
772 | 810 | for i in range(self.startupiterations + 1): # adding one iteration to account for the warmup iteration |
@@ -861,41 +899,43 @@ def run(self): |
861 | 899 | make_archive(archive_base, 'zip', root_dir=logarchive_filename) |
862 | 900 | except Exception as upload_ex: |
863 | 901 | getLogger().warning(f"Failed to save logarchive for diagnosis: {upload_ex}") |
864 | | - # Pull iOS crash reports (.ips) from the device into the upload root, then prune |
865 | | - # to entries relevant to this iteration (matching bundle name OR generated since |
866 | | - # the iteration started). The systemCrashLogs domain exposes /var/mobile/Library/ |
867 | | - # Logs/CrashReporter/, which on shared devices accumulates unrelated reports. |
868 | | - try: |
869 | | - crash_dest = os.path.join(upload_root, f'iteration{i}_crashlogs') |
870 | | - os.makedirs(crash_dest, exist_ok=True) |
871 | | - crashCopyCmd = [ |
872 | | - 'xcrun', 'devicectl', 'device', 'copy', 'from', |
873 | | - '--device', deviceUDID, |
874 | | - '--domain-type', 'systemCrashLogs', |
875 | | - '--source', '/', |
876 | | - '--destination', crash_dest, |
877 | | - ] |
878 | | - getLogger().info(f"Copying device crash logs to {crash_dest} for diagnosis.") |
879 | | - RunCommand(crashCopyCmd, verbose=True).run() |
880 | | - bundle_name = os.path.splitext(os.path.basename(os.path.normpath(self.packagepath)))[0] |
881 | | - iteration_start_ts = runCmdTimestamp.timestamp() |
882 | | - kept = removed = 0 |
883 | | - for root, _, files in os.walk(crash_dest): |
884 | | - for fname in files: |
885 | | - fpath = os.path.join(root, fname) |
| 902 | + # Take a final snapshot and download only crash reports that appeared |
| 903 | + # since the initial snapshot taken before the iteration loop. This |
| 904 | + # matches XHarness's CrashSnapshotReporter pattern and avoids uploading |
| 905 | + # the historical backlog of unrelated crashes the shared device retains. |
| 906 | + # iOS may take a few seconds to finish writing a crash report after the |
| 907 | + # process dies, so poll the snapshot for up to 60s waiting for new |
| 908 | + # entries to appear (matches CrashSnapshotReporter.EndCaptureAsync). |
| 909 | + if initial_device_crashes is None: |
| 910 | + getLogger().info("Skipping device crash log download (initial snapshot unavailable).") |
| 911 | + else: |
| 912 | + crash_wait_deadline = time.time() + 60 |
| 913 | + final_device_crashes = listDeviceCrashReports() |
| 914 | + new_crashes = sorted(final_device_crashes - initial_device_crashes) if final_device_crashes is not None else [] |
| 915 | + while final_device_crashes is not None and not new_crashes and time.time() < crash_wait_deadline: |
| 916 | + time.sleep(1) |
| 917 | + final_device_crashes = listDeviceCrashReports() |
| 918 | + new_crashes = sorted(final_device_crashes - initial_device_crashes) if final_device_crashes is not None else [] |
| 919 | + if final_device_crashes is None: |
| 920 | + getLogger().warning("Skipping device crash log download (final snapshot failed).") |
| 921 | + elif not new_crashes: |
| 922 | + getLogger().info("No new crash reports on device for this test run.") |
| 923 | + else: |
| 924 | + crash_dest = os.path.join(upload_root, f'iteration{i}_crashlogs') |
| 925 | + os.makedirs(crash_dest, exist_ok=True) |
| 926 | + getLogger().info(f"Downloading {len(new_crashes)} new crash report(s) to {crash_dest}.") |
| 927 | + for crash_id in new_crashes: |
| 928 | + dst = os.path.join(crash_dest, os.path.basename(crash_id)) |
| 929 | + dlCmd = xharnesscommand() + [ |
| 930 | + 'apple', 'mlaunch', '--', |
| 931 | + f'--download-crash-report={crash_id}', |
| 932 | + f'--download-crash-report-to={dst}', |
| 933 | + '--devname', deviceUDID, |
| 934 | + ] |
886 | 935 | try: |
887 | | - is_bundle_match = bool(bundle_name) and bundle_name in fname |
888 | | - is_recent = os.path.getmtime(fpath) >= iteration_start_ts |
889 | | - if is_bundle_match or is_recent: |
890 | | - kept += 1 |
891 | | - else: |
892 | | - os.remove(fpath) |
893 | | - removed += 1 |
894 | | - except OSError: |
895 | | - pass |
896 | | - getLogger().info(f"Kept {kept} crash log(s) relevant to {bundle_name!r} or this iteration; pruned {removed}.") |
897 | | - except Exception as crash_ex: |
898 | | - getLogger().warning(f"Failed to copy device crash logs for diagnosis: {crash_ex}") |
| 936 | + RunCommand(dlCmd, verbose=True).run() |
| 937 | + except Exception as dlEx: |
| 938 | + getLogger().warning(f"Failed to download crash report {crash_id}: {dlEx}") |
899 | 939 | raise |
900 | 940 |
|
901 | 941 | # Process Data |
|
0 commit comments