Skip to content

Commit bad02e6

Browse files
matouskozakCopilot
andcommitted
Address review: use mlaunch crash snapshot diff for iOS device
Replace devicectl-based copy-everything-then-filter approach with XHarness's CrashSnapshotReporter pattern, ported to Python: * Before the iteration loop, run 'mlaunch --list-crash-reports' to capture the device's existing crash report set. * In the kill-failure handler, re-list and download only the diff via 'mlaunch --download-crash-report --download-crash-report-to'. * Poll the final snapshot for up to 60s so iOS has time to finish writing the crash report after the process dies (matches CrashSnapshotReporter.EndCaptureAsync). The previous bundle/mtime filter still uploaded the historical backlog of unrelated crashes that accumulate on shared Mac.iPhone.17.Perf devices (8-20 stale .ips per work item seen in build 2986965). The snapshot diff scopes uploads to only the crashes generated during this test run. It's a list+copy, not a move — device state is unchanged, matching XHarness behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 50e6dfe commit bad02e6

1 file changed

Lines changed: 74 additions & 34 deletions

File tree

src/scenarios/shared/runner.py

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import glob
88
import re
9+
import tempfile
910
import time
1011
import json
1112

@@ -767,6 +768,43 @@ def run(self):
767768
RunCommand(installCmd, verbose=True).run()
768769
getLogger().info("Completed install.")
769770

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+
770808
allResults = []
771809
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.
772810
for i in range(self.startupiterations + 1): # adding one iteration to account for the warmup iteration
@@ -861,41 +899,43 @@ def run(self):
861899
make_archive(archive_base, 'zip', root_dir=logarchive_filename)
862900
except Exception as upload_ex:
863901
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+
]
886935
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}")
899939
raise
900940

901941
# Process Data

0 commit comments

Comments
 (0)