xref: /aosp_15_r20/system/extras/simpleperf/scripts/ipc.py (revision 288bf5226967eb3dac5cce6c939ccc2a7f2b4fe5)
1#!/usr/bin/env 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
18"""ipc.py: Capture the Instructions per Cycle (IPC) of the system during a
19           specified duration.
20
21  Example:
22    ./ipc.py
23    ./ipc.py 2 20          # Set interval to 2 secs and total duration to 20 secs
24    ./ipc.py -p 284 -C 4   # Only profile the PID 284 while running on core 4
25    ./ipc.py -c 'sleep 5'  # Only profile the command to run
26
27  Result looks like:
28    K_CYCLES   K_INSTR      IPC
29    36840      14138       0.38
30    70701      27743       0.39
31    104562     41350       0.40
32    138264     54916       0.40
33"""
34
35import io
36import logging
37import subprocess
38import sys
39import time
40
41from simpleperf_utils import (
42        AdbHelper, BaseArgumentParser, get_target_binary_path, log_exit)
43
44def start_profiling(adb, args, target_args):
45    """Start simpleperf process on device."""
46    shell_args = ['simpleperf', 'stat', '-e', 'cpu-cycles',
47            '-e', 'instructions', '--interval', str(args.interval * 1000),
48            '--duration', str(args.duration)]
49    shell_args += target_args
50    adb_args = [adb.adb_path, 'shell'] + shell_args
51    logging.info('run adb cmd: %s' % adb_args)
52    return subprocess.Popen(adb_args, stdout=subprocess.PIPE)
53
54def capture_stats(adb, args, stat_subproc):
55    """Capture IPC profiling stats or stop profiling when user presses Ctrl-C."""
56    try:
57        print("%-10s %-10s %5s" % ("K_CYCLES", "K_INSTR", "IPC"))
58        cpu_cycles = 0
59        for line in io.TextIOWrapper(stat_subproc.stdout, encoding="utf-8"):
60            if 'cpu-cycles' in line:
61                if args.cpu == None:
62                    cpu_cycles = int(line.split()[0].replace(",", ""))
63                    continue
64                columns = line.split()
65                if args.cpu == int(columns[0]):
66                    cpu_cycles = int(columns[1].replace(",", ""))
67            elif 'instructions' in line:
68                if cpu_cycles == 0: cpu_cycles = 1 # PMCs are broken, or no events
69                ins = -1
70                columns = line.split()
71                if args.cpu == None:
72                    ins = int(columns[0].replace(",", ""))
73                elif args.cpu == int(columns[0]):
74                    ins = int(columns[1].replace(",", ""))
75                if ins >= 0:
76                    print("%-10d %-10d %5.2f" %
77                            (cpu_cycles / 1000, ins / 1000, ins / cpu_cycles))
78
79    except KeyboardInterrupt:
80        stop_profiling(adb)
81        stat_subproc = None
82
83def stop_profiling(adb):
84    """Stop profiling by sending SIGINT to simpleperf and wait until it exits."""
85    has_killed = False
86    while True:
87        (result, _) = adb.run_and_return_output(['shell', 'pidof', 'simpleperf'])
88        if not result:
89            break
90        if not has_killed:
91            has_killed = True
92            adb.run_and_return_output(['shell', 'pkill', '-l', '2', 'simpleperf'])
93        time.sleep(1)
94
95def capture_ipc(args):
96    # Initialize adb and verify device
97    adb = AdbHelper(enable_switch_to_root=True)
98    if not adb.is_device_available():
99        log_exit('No Android device is connected via ADB.')
100    is_root_device = adb.switch_to_root()
101    device_arch = adb.get_device_arch()
102
103    if args.pid:
104       (result, _) = adb.run_and_return_output(['shell', 'ls', '/proc/%s' % args.pid])
105       if not result:
106           log_exit("Pid '%s' does not exist" % args.pid)
107
108    target_args = []
109    if args.cpu is not None:
110        target_args += ['--per-core']
111    if args.pid:
112        target_args += ['-p', args.pid]
113    elif args.command:
114        target_args += [args.command]
115    else:
116        target_args += ['-a']
117
118    stat_subproc = start_profiling(adb, args, target_args)
119    capture_stats(adb, args, stat_subproc)
120
121def main():
122    parser = BaseArgumentParser(description=__doc__)
123    parser.add_argument('-C', '--cpu', type=int, help='Capture IPC only for this CPU core')
124    process_group = parser.add_mutually_exclusive_group()
125    process_group.add_argument('-p', '--pid', help='Capture IPC only for this PID')
126    process_group.add_argument('-c', '--command', help='Capture IPC only for this command')
127    parser.add_argument('interval', nargs='?', default=1, type=int, help='sampling interval in seconds')
128    parser.add_argument('duration', nargs='?', default=10, type=int, help='sampling duration in seconds')
129
130    args = parser.parse_args()
131    if args.interval > args.duration:
132        log_exit("interval cannot be greater than duration")
133
134    capture_ipc(args)
135
136if __name__ == '__main__':
137    main()
138