8
8
import glob
9
9
import re
10
10
import time
11
+ from datetime import datetime
12
+ import json
11
13
12
14
from logging import exception , getLogger
13
15
from collections import namedtuple
14
16
from argparse import ArgumentParser
15
17
from argparse import RawTextHelpFormatter
16
18
from io import StringIO
17
- from shutil import move
19
+ from shutil import move , rmtree
18
20
from shared .crossgen import CrossgenArguments
19
21
from shared .startup import StartupWrapper
20
22
from shared .util import publishedexe , pythoncommand , appfolder , xharnesscommand
@@ -57,12 +59,12 @@ def parseargs(self):
57
59
58
60
# parse only command
59
61
parseonlyparser = subparsers .add_parser (const .DEVICESTARTUP ,
60
- description = 'measure time to main for Android apps' )
62
+ description = 'measure time to startup for Android/iOS apps' )
61
63
parseonlyparser .add_argument ('--device-type' , choices = ['android' ,'ios' ],type = str .lower ,help = 'Device type for testing' , dest = 'devicetype' )
62
64
parseonlyparser .add_argument ('--package-path' , help = 'Location of test application' , dest = 'packagepath' )
63
- parseonlyparser .add_argument ('--package-name' , help = 'Classname of application' , dest = 'packagename' )
65
+ parseonlyparser .add_argument ('--package-name' , help = 'Classname (Android) or Bundle ID (iOS) of application' , dest = 'packagename' )
64
66
parseonlyparser .add_argument ('--startup-iterations' , help = 'Startups to run (1+)' , type = int , default = 10 , dest = 'startupiterations' )
65
- parseonlyparser .add_argument ('--disable-animations' , help = 'Disable Android device animations' , action = 'store_true' , dest = 'animationsdisabled' )
67
+ parseonlyparser .add_argument ('--disable-animations' , help = 'Disable Android device animations, does nothing on iOS. ' , action = 'store_true' , dest = 'animationsdisabled' )
66
68
self .add_common_arguments (parseonlyparser )
67
69
68
70
# inner loop command
@@ -306,7 +308,7 @@ def run(self):
306
308
startup .runtests (self .traits )
307
309
308
310
309
- elif self .testtype == const .DEVICESTARTUP :
311
+ elif self .testtype == const .DEVICESTARTUP and self . devicetype == 'android' :
310
312
# ADB Key Event corresponding numbers: https://gist.github.com/arjunv/2bbcca9a1a1c127749f8dcb6d36fb0bc
311
313
# Regex used to split the response from starting the activity and saving each value
312
314
#Example:
@@ -327,7 +329,7 @@ def run(self):
327
329
getLogger ().info ("Removed: " + os .path .join (const .TRACEDIR , file ))
328
330
os .remove (file )
329
331
330
- cmdline = xharnesscommand () + [self . devicetype , 'state' , '--adb' ]
332
+ cmdline = xharnesscommand () + ['android' , 'state' , '--adb' ]
331
333
adb = RunCommand (cmdline , verbose = True )
332
334
adb .run ()
333
335
@@ -427,7 +429,7 @@ def run(self):
427
429
getLogger ().info (f"Animation values successfully set to { animationValue } ." )
428
430
429
431
installCmd = xharnesscommand () + [
430
- self . devicetype ,
432
+ 'android' ,
431
433
'install' ,
432
434
'--app' , self .packagepath ,
433
435
'--package-name' ,
@@ -588,6 +590,184 @@ def run(self):
588
590
self .traits .add_traits (overwrite = True , apptorun = "app" , startupmetric = const .STARTUP_DEVICETIMETOMAIN , tracefolder = 'PerfTest/' , tracename = 'runoutput.trace' , scenarioname = self .scenarioname )
589
591
startup .parsetraces (self .traits )
590
592
593
+ elif self .testtype == const .DEVICESTARTUP and self .devicetype == 'ios' :
594
+
595
+ getLogger ().info ("Clearing potential previous run nettraces" )
596
+ for file in glob .glob (os .path .join (const .TRACEDIR , 'PerfTest' , 'runoutput.trace' )):
597
+ if exists (file ):
598
+ getLogger ().info ("Removed: " + os .path .join (const .TRACEDIR , file ))
599
+ os .remove (file )
600
+
601
+ if not exists (const .TMPDIR ):
602
+ os .mkdir (const .TMPDIR )
603
+
604
+ getLogger ().info ("Clearing potential previous run *.logarchive" )
605
+ for logarchive in glob .glob (os .path .join (const .TMPDIR , '*.logarchive' )):
606
+ if exists (logarchive ):
607
+ getLogger ().info ("Removed: " + os .path .join (const .TMPDIR , logarchive ))
608
+ rmtree (logarchive )
609
+
610
+ getLogger ().info ("Checking device state." )
611
+ cmdline = xharnesscommand () + ['apple' , 'state' ]
612
+ adb = RunCommand (cmdline , verbose = True )
613
+ adb .run ()
614
+
615
+ getLogger ().info ("Installing app on device." )
616
+ installCmd = xharnesscommand () + [
617
+ 'apple' ,
618
+ 'install' ,
619
+ '--app' , self .packagepath ,
620
+ '--target' , 'ios-device' ,
621
+ '-o' ,
622
+ const .TRACEDIR ,
623
+ '-v'
624
+ ]
625
+ RunCommand (installCmd , verbose = True ).run ()
626
+ getLogger ().info ("Completed install." )
627
+
628
+ allResults = []
629
+ for i in range (self .startupiterations + 1 ): # adding one iteration to account for the warmup iteration
630
+ getLogger ().info ("Waiting 10 secs to ensure we're not getting confused with previous app run." )
631
+ time .sleep (10 )
632
+
633
+ getLogger ().info (f"Collect startup data for iteration { i } ." )
634
+ runCmdTimestamp = datetime .now ()
635
+ runCmd = xharnesscommand () + [
636
+ 'apple' ,
637
+ 'mlaunch' ,
638
+ '--' ,
639
+ f'--launchdevbundleid={ self .packagename } ' ,
640
+ ]
641
+ runCmdCommand = RunCommand (runCmd , verbose = True )
642
+ runCmdCommand .run ()
643
+ app_pid_search = re .search ("Launched application.*with pid (?P<app_pid>\d+)" , runCmdCommand .stdout )
644
+ app_pid = int (app_pid_search .group ('app_pid' ))
645
+
646
+ logarchive_filename = os .path .join (const .TMPDIR , f'iteration{ i } .logarchive' )
647
+ getLogger ().info (f"Waiting 5 secs to ensure app with PID { app_pid } is fully started." )
648
+ time .sleep (5 )
649
+ collectCmd = [
650
+ 'sudo' ,
651
+ 'log' ,
652
+ 'collect' ,
653
+ '--device' ,
654
+ '--start' , runCmdTimestamp .strftime ("%Y-%m-%d %H:%M:%S" ),
655
+ '--output' , logarchive_filename ,
656
+ ]
657
+ RunCommand (collectCmd , verbose = True ).run ()
658
+
659
+ getLogger ().info (f"Kill app with PID { app_pid } ." )
660
+ killCmd = xharnesscommand () + [
661
+ 'apple' ,
662
+ 'mlaunch' ,
663
+ '--' ,
664
+ f'--killdev={ app_pid } ' ,
665
+ ]
666
+ killCmdCommand = RunCommand (killCmd , verbose = True )
667
+ killCmdCommand .run ()
668
+
669
+ # Process Data
670
+
671
+ # There are four watchdog events from SpringBoard during an application startup:
672
+ #
673
+ # [application<net.dot.maui>:770] [realTime] Now monitoring resource allowance of 20.00s (at refreshInterval -1.00s)
674
+ # [application<net.dot.maui>:770] [realTime] Stopped monitoring.
675
+ # [application<net.dot.maui>:770] [realTime] Now monitoring resource allowance of 19.28s (at refreshInterval -1.00s)
676
+ # [application<net.dot.maui>:770] [realTime] Stopped monitoring.
677
+ #
678
+ # The first two are monitoring the time it takes the OS to create the process, load .dylibs and call into the app's main()
679
+ # The second two are monitoring the time it takes the app to draw the first frame of UI from main()
680
+ #
681
+ # An app has 20 seconds to complete this sequence or the OS will kill the app.
682
+ # We collect these log events to do our measurements.
683
+
684
+ logShowCmd = [
685
+ 'log' ,
686
+ 'show' ,
687
+ '--predicate' , '(process == "SpringBoard") && (category == "Watchdog")' ,
688
+ '--info' ,
689
+ '--style' , 'ndjson' ,
690
+ logarchive_filename ,
691
+ ]
692
+ logShowCmdCommand = RunCommand (logShowCmd , verbose = True )
693
+ logShowCmdCommand .run ()
694
+
695
+ events = []
696
+ for line in logShowCmdCommand .stdout .splitlines ():
697
+ try :
698
+ lineData = json .loads (line )
699
+ if 'Now monitoring resource allowance' in lineData ['eventMessage' ] or 'Stopped monitoring' in lineData ['eventMessage' ]:
700
+ events .append (lineData )
701
+ except :
702
+ break
703
+
704
+ # the startup measurement relies on the date/time of the device to be pretty much in sync with the host
705
+ # since we use the timestamps from the host to decide which parts of the device log to get and
706
+ # we then use that to calculate the time delta from watchdog events
707
+ if len (events ) != 4 :
708
+ raise Exception ("Didn't get the right amount of watchdog events, this could mean the app crashed or the device clock is not in sync with the host." )
709
+
710
+ timeToMainEventStart = events [0 ]
711
+ timeToMainEventStop = events [1 ]
712
+ timeToFirstDrawEventStart = events [2 ]
713
+ timeToFirstDrawEventStop = events [3 ]
714
+
715
+ # validate log messages
716
+ if f'application<{ self .packagename } >:{ app_pid } ' not in timeToMainEventStart ['eventMessage' ] or 'Now monitoring resource allowance of 20.00s' not in timeToMainEventStart ['eventMessage' ]:
717
+ raise Exception (f"Invalid timeToMainEventStart: { timeToMainEventStart ['eventMessage' ]} " )
718
+
719
+ if f'application<{ self .packagename } >:{ app_pid } ' not in timeToMainEventStop ['eventMessage' ] or 'Stopped monitoring' not in timeToMainEventStop ['eventMessage' ]:
720
+ raise Exception (f"Invalid timeToMainEventStop: { timeToMainEventStop ['eventMessage' ]} " )
721
+
722
+ if f'application<{ self .packagename } >:{ app_pid } ' not in timeToFirstDrawEventStart ['eventMessage' ] or 'Now monitoring resource allowance of' not in timeToFirstDrawEventStart ['eventMessage' ]:
723
+ raise Exception (f"Invalid timeToFirstDrawEventStart: { timeToFirstDrawEventStart ['eventMessage' ]} " )
724
+
725
+ if f'application<{ self .packagename } >:{ app_pid } ' not in timeToFirstDrawEventStop ['eventMessage' ] or 'Stopped monitoring' not in timeToFirstDrawEventStop ['eventMessage' ]:
726
+ raise Exception (f"Invalid timeToFirstDrawEventStop: { timeToFirstDrawEventStop ['eventMessage' ]} " )
727
+
728
+ timeToMainEventStartDateTime = datetime .strptime (timeToMainEventStart ['timestamp' ], '%Y-%m-%d %H:%M:%S.%f%z' )
729
+ timeToMainEventEndDateTime = datetime .strptime (timeToMainEventStop ['timestamp' ], '%Y-%m-%d %H:%M:%S.%f%z' )
730
+ timeToMainMilliseconds = (timeToMainEventEndDateTime - timeToMainEventStartDateTime ).total_seconds () * 1000
731
+
732
+ timeToFirstDrawEventStartDateTime = datetime .strptime (timeToFirstDrawEventStart ['timestamp' ], '%Y-%m-%d %H:%M:%S.%f%z' )
733
+ timeToFirstDrawEventEndDateTime = datetime .strptime (timeToFirstDrawEventStop ['timestamp' ], '%Y-%m-%d %H:%M:%S.%f%z' )
734
+ timeToFirstDrawMilliseconds = (timeToFirstDrawEventEndDateTime - timeToFirstDrawEventStartDateTime ).total_seconds () * 1000
735
+
736
+ totalTimeMilliseconds = timeToMainMilliseconds + timeToFirstDrawMilliseconds
737
+
738
+ if i == 0 :
739
+ # ignore the warmup iteration
740
+ getLogger ().info (f'Warmup iteration took { totalTimeMilliseconds } ' )
741
+ else :
742
+ # TODO: this isn't really a COLD run, we should have separate measurements for starting the app right after install
743
+ launchState = 'COLD'
744
+ allResults .append (f'LaunchState: { launchState } \n TotalTime: { int (totalTimeMilliseconds )} \n TimeToMain: { int (timeToMainMilliseconds )} \n \n ' )
745
+
746
+ # Done with testing, uninstall the app
747
+ getLogger ().info ("Uninstalling app" )
748
+ uninstallAppCmd = xharnesscommand () + [
749
+ 'apple' ,
750
+ 'uninstall' ,
751
+ '--app' , self .packagename ,
752
+ '--target' , 'ios-device' ,
753
+ '-o' ,
754
+ const .TRACEDIR ,
755
+ '-v'
756
+ ]
757
+ RunCommand (uninstallAppCmd , verbose = True ).run ()
758
+
759
+ # Create traces to store the data so we can keep the current general parse trace flow
760
+ getLogger ().info (f"Logs: \n { allResults } " )
761
+ os .makedirs (f"{ const .TRACEDIR } /PerfTest" , exist_ok = True )
762
+ traceFile = open (f"{ const .TRACEDIR } /PerfTest/runoutput.trace" , "w" )
763
+ for result in allResults :
764
+ traceFile .write (result )
765
+ traceFile .close ()
766
+
767
+ startup = StartupWrapper ()
768
+ self .traits .add_traits (overwrite = True , apptorun = "app" , startupmetric = const .STARTUP_DEVICETIMETOMAIN , tracefolder = 'PerfTest/' , tracename = 'runoutput.trace' , scenarioname = self .scenarioname )
769
+ startup .parsetraces (self .traits )
770
+
591
771
elif self .testtype == const .SOD :
592
772
sod = SODWrapper ()
593
773
builtdir = const .PUBDIR if os .path .exists (const .PUBDIR ) else None
0 commit comments