trace抓取工具

文章目录

python代码

bash 复制代码
#!/usr/bin/env python3
# Copyright (C) 2021 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import atexit
import argparse
import datetime
import hashlib
import http.server
import os
import re
import shutil
import signal
import socketserver
import subprocess
import sys
import time
import webbrowser
import threading
import random # 【补充缺失的 import】
import platform # 【补充缺失的 import】

# ----- Amalgamator: begin of python/perfetto/prebuilts/manifests/tracebox.py
TRACEBOX_MANIFEST = [{
    'arch': 'mac-amd64',
    'file_name': 'tracebox',
    'file_size': 1646808,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-amd64/tracebox',
    'sha256': '85b3060ed4d49e2c8d69dbb4d6ff26ab662f9b28c0032791674c90683dd33d39',
    'platform': 'darwin',
    'machine': ['x86_64']
}, {
    'arch': 'mac-arm64',
    'file_name': 'tracebox',
    'file_size': 1508856,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/mac-arm64/tracebox',
    'sha256': 'ea2cce845daf0eba469ff356b3bcefc8e9a384084569271a470b58a9dcbf8def',
    'platform': 'darwin',
    'machine': ['arm64']
}, {
    'arch': 'linux-amd64',
    'file_name': 'tracebox',
    'file_size': 2415168,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-amd64/tracebox',
    'sha256': '5361676fb3c2490ae2136ab7a37dcd9e4ee5a2a6c0ba722facf3215a23a8c633',
    'platform': 'linux',
    'machine': ['x86_64']
}, {
    'arch': 'linux-arm',
    'file_name': 'tracebox',
    'file_size': 1478024,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm/tracebox',
    'sha256': '18db321576be555d8c9281df9fc03aa6b3b4358ae2424ffbd65fc120cd650b8b',
    'platform': 'linux',
    'machine': ['armv6l', 'armv7l', 'armv8l']
}, {
    'arch': 'linux-arm64',
    'file_name': 'tracebox',
    'file_size': 2304384,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/linux-arm64/tracebox',
    'sha256': '70c9e2b63eb92a82db65916c346b09867bfedc0c4593974c019102f485c0dc9d',
    'platform': 'linux',
    'machine': ['aarch64']
}, {
    'arch': 'android-arm',
    'file_name': 'tracebox',
    'file_size': 1354916,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm/tracebox',
    'sha256': '724a1cb4774bdf8a64beb37194f7394df5a052c36369ea52f64fe519fcb40117'
}, {
    'arch': 'android-arm64',
    'file_name': 'tracebox',
    'file_size': 2142008,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-arm64/tracebox',
    'sha256': '7616bfc3be1269c3ac1eec5a1f868fb65c2830ed001b5fbcc3800c909c676848'
}, {
    'arch': 'android-x86',
    'file_name': 'tracebox',
    'file_size': 2341884,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x86/tracebox',
    'sha256': '29124bee9bf4e2e296b0c96071b8c9706b57d963cbf0359d6afd95a9049b2b82'
}, {
    'arch': 'android-x64',
    'file_name': 'tracebox',
    'file_size': 2178416,
    'url': 'https://commondatastorage.googleapis.com/perfetto-luci-artifacts/v49.0/android-x64/tracebox',
    'sha256': '826fffce1e138c1d5ac107492ee696c09ad83f9ae9aa647c810d71084f797509'
}]
# ----- Amalgamator: end of python/perfetto/prebuilts/manifests/tracebox.py

def download_or_get_cached(file_name, url, sha256):
  dir = os.path.join(
      os.path.expanduser('~'), '.local', 'share', 'perfetto', 'prebuilts')
  os.makedirs(dir, exist_ok=True)
  bin_path = os.path.join(dir, file_name)
  sha256_path = os.path.join(dir, file_name + '.sha256')
  needs_download = True

  if os.path.exists(bin_path) and os.path.exists(sha256_path):
    with open(sha256_path, 'rb') as f:
      digest = f.read().decode()
      if digest == sha256:
        needs_download = False

  if needs_download: 
    tmp_path = '%s.%d.tmp' % (bin_path, random.randint(0, 100000))
    print('Downloading ' + url)
    subprocess.check_call(['curl', '-f', '-L', '-#', '-o', tmp_path, url])
    with open(tmp_path, 'rb') as fd:
      actual_sha256 = hashlib.sha256(fd.read()).hexdigest()
    if actual_sha256 != sha256:
      raise Exception('Checksum mismatch for %s (actual: %s, expected: %s)' %
                      (url, actual_sha256, sha256))
    os.chmod(tmp_path, 0o755)
    os.replace(tmp_path, bin_path)
    with open(tmp_path, 'w') as f:
      f.write(sha256)
    os.replace(tmp_path, sha256_path)
  return bin_path

def get_perfetto_prebuilt(manifest, soft_fail=False, arch=None):
  plat = sys.platform.lower()
  machine = platform.machine().lower()
  manifest_entry = None
  for entry in manifest:
    if arch:
      if entry.get('arch') == arch:
        manifest_entry = entry
        break
      continue
    if entry.get('platform') == plat and machine in entry.get('machine', []):
      manifest_entry = entry
      break
  if manifest_entry is None:
    if soft_fail:
      return None
    raise Exception(
        ('No prebuilts available for %s-%s\n' % (plat, machine)) +
        'See https://perfetto.dev/docs/contributing/build-instructions')

  return download_or_get_cached(
      file_name=manifest_entry['file_name'],
      url=manifest_entry['url'],
      sha256=manifest_entry['sha256'])

def run_perfetto_prebuilt(manifest):
  bin_path = get_perfetto_prebuilt(manifest)
  if sys.platform.lower() == 'win32':
    sys.exit(subprocess.check_call([bin_path, *sys.argv[1:]]))
  os.execv(bin_path, [bin_path] + sys.argv[1:])

def repo_root():
  path = os.path.dirname(os.path.abspath(__file__)) 
  last_dir = ''
  while path and path != last_dir:
    if os.path.exists(os.path.join(path, 'perfetto.rc')):
      return path
    last_dir = path
    path = os.path.dirname(path)
  return None

def repo_dir(rel_path):
  return os.path.join(repo_root() or '', rel_path)

HERMETIC_ADB_PATH = repo_dir('buildtools/android_sdk/platform-tools/adb')

ABI_TO_ARCH = {
    'armeabi-v7a': 'arm',
    'arm64-v8a': 'arm64',
    'x86': 'x86',
    'x86_64': 'x64',
}

MAX_ADB_FAILURES = 15

devnull = open(os.devnull, 'rb')
adb_path = None
procs = []

class ANSI:
  END = '\033[0m'
  BOLD = '\033[1m'
  RED = '\033[91m'
  BLACK = '\033[30m'
  BLUE = '\033[94m'
  BG_YELLOW = '\033[43m'
  BG_BLUE = '\033[44m'
  GREEN = '\033[92m'
  YELLOW = '\033[93m' # 【新增颜色】

# 【修改优化:结构化日志输出格式 [INFO] / [ERROR] 等】
def log_msg(msg, color=ANSI.END, prefix="[INFO]"):
  """Format logs with timestamp prefix."""
  ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  # 结构化输出
  print(f"{color}{prefix} [{ts}] {msg}{ANSI.END}")

class HttpHandler(http.server.SimpleHTTPRequestHandler):
  def end_headers(self):
    self.send_header('Access-Control-Allow-Origin', self.server.allow_origin)
    self.send_header('Cache-Control', 'no-cache')
    super().end_headers()

  def do_GET(self):
    if self.path != '/' + self.server.expected_fname:
      self.send_error(404, "File not found")
      return
    self.server.fname_get_completed = True
    super().do_GET()

  def do_POST(self):
    self.send_error(404, "File not found")

  # 【新增这一段】屏蔽默认的 HTTP 请求日志输出
  def log_message(self, format, *args):
    pass

def select_device(serial_arg):
  """Handle multiple devices selection."""
  proc = subprocess.Popen([adb_path, 'devices'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  out, _ = proc.communicate()
  lines = out.decode('utf-8').strip().split('\n')[1:]
  devices = [line.split()[0] for line in lines if 'device' in line and not line.startswith('*')]

  if serial_arg:
    if serial_arg not in devices and devices:
      log_msg(f'Specified device {serial_arg} not found in connected devices.', ANSI.BG_YELLOW, "[WARN]")
    return serial_arg

  if len(devices) == 0:
    log_msg('No adb devices found. Please connect a device.', ANSI.RED, "[ERROR]")
    sys.exit(1)
  elif len(devices) == 1:
    return devices[0]
  else:
    # 【优化易用性】默认选择第一个设备,不再阻塞脚本运行
    log_msg(f'Multiple devices detected. Auto-selecting the first one: {devices[0]}', ANSI.YELLOW, "[WARN]")
    return devices[0]

# 【新增优化:生成性能分析模板配置】
def generate_scene_config(args):
    duration_ms = 10000
    if args.time and args.time.endswith('s'):
        duration_ms = int(args.time[:-1]) * 1000

    buffer_kb = 32768
    if args.buffer and args.buffer.endswith('mb'):
        buffer_kb = int(args.buffer[:-2]) * 1024

    # 定义模板
    categories = []
    if args.scene == 'jank':
        categories = ["gfx", "input", "view", "wm", "am", "sm", "hal", "res", "dalvik", "rs", "bionic", "binder_driver", "binder_lock"]
    elif args.scene == 'launch':
        categories = ["am", "wm", "gfx", "view", "binder_driver", "sched", "freq", "idle", "res", "dalvik", "ss"]
    elif args.scene == 'cpu':
        categories = ["sched", "freq", "idle"]

    # 生成动态 TextProto
    config_text = f"""
buffers: {{ size_kb: {buffer_kb} fill_policy: RING_BUFFER }}
data_sources: {{
    config {{
        name: "linux.sys_stats"
        sys_stats_config {{ stat_period_ms: 250 stat_counters: STAT_CPU_TIMES stat_counters: STAT_FORK_COUNT }}
    }}
}}
data_sources: {{
    config {{
        name: "linux.ftrace"
        ftrace_config {{
            ftrace_events: "sched/sched_switch"
            ftrace_events: "power/cpu_frequency"
            ftrace_events: "power/cpu_idle"
            ftrace_events: "sched/sched_wakeup"
            ftrace_events: "sched/sched_wakeup_new"
            ftrace_events: "sched/sched_waking"
"""
    for cat in categories:
        config_text += f'            atrace_categories: "{cat}"\n'
    if args.pkg:
        config_text += f'            atrace_apps: "{args.pkg}"\n'

    config_text += f"""        }}
    }}
}}
duration_ms: {duration_ms}
"""
    config_path = "auto_generated_scene.pbtx"
    with open(config_path, "w") as f:
        f.write(config_text)
    return config_path


def setup_arguments():
  atexit.register(kill_all_subprocs_on_exit)
  default_out_dir_str = './'
  default_out_dir = os.path.expanduser(default_out_dir_str)

  examples = '\n'.join([
      ANSI.BOLD + 'Examples' + ANSI.END, '  -t 10s -b 32mb sched gfx wm -a*',
      '  -t 5s sched/sched_switch raw_syscalls/sys_enter raw_syscalls/sys_exit',
      '  -c /path/to/full-textual-trace.config', '',
      ANSI.BOLD + 'Long traces' + ANSI.END,
      'If you want to record a hours long trace and stream it into a file ',
      'you need to pass a full trace config and set write_into_file = true.',
      'See https://perfetto.dev/docs/concepts/config#long-traces .'
  ])
  parser = argparse.ArgumentParser(
      epilog=examples, formatter_class=argparse.RawTextHelpFormatter)

  help = 'Output file or directory (default: %s)' % default_out_dir_str
  parser.add_argument('-o', '--out', default=default_out_dir, help=help)

  help = 'Don\'t open or serve the trace'
  parser.add_argument('-n', '--no-open', action='store_true', help=help)

  help = 'Don\'t open in browser, but still serve trace (good for remote use)'
  parser.add_argument('--no-open-browser', action='store_true', help=help)

  help = 'The web address used to open trace files'
  parser.add_argument('--origin', default='https://ui.perfetto.dev', help=help)

  help = 'Force the use of the sideloaded binaries rather than system daemons'
  parser.add_argument('--sideload', action='store_true', help=help)

  help = ('Sideload the given binary rather than downloading it. ' +
          'Implies --sideload')
  parser.add_argument('--sideload-path', default=None, help=help)

  help = 'Ignores any tracing guardrails which might be used'
  parser.add_argument('--no-guardrails', action='store_true', help=help)

  help = 'Don\'t run `adb root` run as user (only when sideloading)'
  parser.add_argument('-u', '--user', action='store_true', help=help)

  help = 'Specify the ADB device serial'
  parser.add_argument('--serial', '-s', default=None, help=help)

  # 【新增优化:自动化与场景扩展参数】
  grp_auto = parser.add_argument_group('Automation Options')
  grp_auto.add_argument('--scene', choices=['jank', 'launch', 'cpu', 'none'], default='none', help='预设性能分析场景(自动生成配置)')
  grp_auto.add_argument('--pkg', default='', help='目标包名 (配合 --scene 使用)')
  grp_auto.add_argument('--loop', type=int, default=1, help='循环抓取次数 (用于复现难复现的问题)')

  grp = parser.add_argument_group(
      'Short options: (only when not using -c/--config)')

  help = 'Trace duration N[s,m,h] (default: 10s)' # 【优化:修改默认时长为 10s 提升易用性】
  grp.add_argument('-t', '--time', default='10s', help=help)

  help = 'Ring buffer size N[mb,gb] (default: 32mb)'
  grp.add_argument('-b', '--buffer', default='32mb', help=help)

  help = ('Android (atrace) app names. Can be specified multiple times.\n-a*' +
          'for all apps (without space between a and * or bash will expand it)')
  grp.add_argument(
      '-a',
      '--app',
      metavar='com.myapp',
      action='append',
      default=[],
      help=help)

  help = 'sched, gfx, am, wm (see --list)'
  grp.add_argument('events', metavar='Atrace events', nargs='*', help=help)

  help = 'sched/sched_switch kmem/kmem (see --list-ftrace)'
  grp.add_argument('_', metavar='Ftrace events', nargs='*', help=help)

  help = 'Lists all the categories available'
  grp.add_argument('--list', action='store_true', help=help)

  help = 'Lists all the ftrace events available'
  grp.add_argument('--list-ftrace', action='store_true', help=help)

  section = ('Full trace config (only when not using short options)')
  grp = parser.add_argument_group(section)

  help = 'Can be generated with https://ui.perfetto.dev/#!/record'
  grp.add_argument('-c', '--config', default=None, help=help)

  help = 'Parse input from --config as binary proto (default: parse as text)'
  grp.add_argument('--bin', action='store_true', help=help)

  help = ('Pass the trace through the trace reporter API. Only works when '
          'using the full trace config (-c) with the reporter package name '
          "'android.perfetto.cts.reporter' and the reporter class name "
          "'android.perfetto.cts.reporter.PerfettoReportService' with the "
          'reporter installed on the device (see '
          'tools/install_test_reporter_app.py).')
  grp.add_argument('--reporter-api', action='store_true', help=help)

  args = parser.parse_args()
  args.sideload = args.sideload or args.sideload_path is not None

  # 【新增优化:配置加载的优先级与容错机制】
  if args.config is None:
    # 优先级 1:如果指定了场景模板,使用动态生成的配置
    if args.scene != 'none':
      args.config = generate_scene_config(args)
      args.bin = False # 强制为 text 模式
      log_msg(f"已应用场景模板: {args.scene}, 包名: {args.pkg or '全局'}", ANSI.GREEN, "[INFO]")
    else:
      # 优先级 2:优先检测并使用当前目录下的 syshealth_google.pbtx
      default_pbtx = os.path.join(os.getcwd(), 'syshealth_google.pbtx')
      if os.path.exists(default_pbtx):
        args.config = default_pbtx
        log_msg(f"自动加载当前目录配置: syshealth_google.pbtx", ANSI.GREEN, "[INFO]")
      else:
        # 优先级 3 (容错兜底):既没传参,也没场景,当前目录也没默认文件,进入交互式提示
        ts = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        log_msg("未检测到预设配置(--scene)或默认配置文件(syshealth_google.pbtx)。", ANSI.YELLOW, "[WARN]")
        user_input = input(f"[{ts}] 请输入 config 文件的绝对/相对路径 (直接按回车将退出): ").strip()
        
        if user_input:
          args.config = user_input
        else:
          log_msg("未提供有效的配置文件,抓取取消。", ANSI.RED, "[ERROR]")
          sys.exit(1)

  # Setup adb_path first, then run smart device selection
  find_adb()
  args.serial = select_device(args.serial)
  os.environ["ANDROID_SERIAL"] = args.serial

  if args.list:
    adb('shell', 'atrace', '--list_categories').wait()
    sys.exit(0)

  if args.list_ftrace:
    adb('shell', 'cat /d/tracing/available_events | tr : /').wait()
    sys.exit(0)

  # 【容错增强:最后校验配置文件的有效性】
  if not os.path.exists(args.config):
    log_msg(f"致命错误:配置文件不存在 -> {args.config}", ANSI.RED, "[ERROR]")
    sys.exit(1)

  return args

class SignalException(Exception):
  pass

def signal_handler(sig, frame):
  raise SignalException('Received signal ' + str(sig))

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)


def start_trace(args, print_log=True):
  perfetto_cmd = 'perfetto'
  device_dir = '/data/misc/perfetto-traces/'
  # 【修改点 1】更改 remote_config_path 为官方推荐的具有 SELinux 读取权限的目录
  remote_config_path = '/data/misc/perfetto-configs/perfetto.conf'

  if print_log:
      log_msg(f"设备信息:{args.serial}", ANSI.END, "[INFO]")
      log_msg(f"包名:{args.pkg if hasattr(args, 'pkg') and args.pkg else '未指定'}", ANSI.END, "[INFO]")

  # 【修改点 2】确保目标配置目录存在,防止某些精简系统下找不到文件夹
  adb('shell', 'mkdir -p /data/misc/perfetto-configs').wait()

  # Check existing traces
  if print_log: log_msg("Checking for existing Perfetto traces...")
  check_cmd = adb('shell', 'ls /data/misc/perfetto-traces/*.pftrace 2>/dev/null', stdout=subprocess.PIPE)
  out, _ = check_cmd.communicate()
  if out.strip():
    if print_log: log_msg("Existing Perfetto trace found. Cleaning up...", ANSI.BG_YELLOW, "[WARN]")
    adb('shell', 'rm -f /data/misc/perfetto-traces/*.pftrace').wait()

  probe_cmd = 'getprop ro.build.version.sdk; getprop ro.product.cpu.abi; whoami'
  probe = adb('shell', probe_cmd, stdout=subprocess.PIPE)
  lines = probe.communicate()[0].decode().strip().split('\n')
  lines = [x.strip() for x in lines] 
  if probe.returncode != 0:
    log_msg('未检测到设备,或 ADB 异常', ANSI.RED, "[ERROR]")
    sys.exit(1)
  
  api_level = int(lines[0])
  abi = lines[1]
  arch = ABI_TO_ARCH.get(abi)
  if arch is None:
    log_msg('Unsupported ABI: ' + abi, ANSI.RED, "[ERROR]")
    sys.exit(1)
  
  shell_user = lines[2]
  if api_level < 29 or args.sideload: 
    tracebox_bin = args.sideload_path
    if tracebox_bin is None:
      tracebox_bin = get_perfetto_prebuilt(
          TRACEBOX_MANIFEST, arch='android-' + arch)
    perfetto_cmd = '/data/local/tmp/tracebox'
    exit_code = adb('push', '--sync', tracebox_bin, perfetto_cmd).wait()
    exit_code |= adb('shell', 'chmod 755 ' + perfetto_cmd).wait()
    if exit_code != 0:
      log_msg('ADB push failed', ANSI.RED, "[ERROR]")
      sys.exit(1)
    device_dir = '/data/local/tmp/'
    if shell_user != 'root' and not args.user:
      adb('root').wait()

    # Push Config
  if print_log: log_msg(f"开始下发配置到 {remote_config_path}...", ANSI.END, "[INFO]")
  push_cmd = adb('push', args.config, remote_config_path, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  push_out, _ = push_cmd.communicate()

  # 【修改优化:文件命名改为 设备号_时间.perfetto-trace,格式更清爽】
  tstamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
  # 使用 args.serial 获取设备号
  fname = f'{args.serial}_{tstamp}.perfetto-trace'
  device_file = device_dir + fname

  # Construct Perfetto command using the pushed config
  cmd = [perfetto_cmd, '--background', '-c', remote_config_path]
  if not args.bin:
    cmd.append('--txt')

  if args.no_guardrails:
    cmd.append('--no-guardrails')

  if args.reporter_api:
    adb('shell', 'rm /sdcard/Android/data/android.perfetto.cts.reporter/files/*').wait()
    cmd.append('--upload')
  else:
    cmd.extend(['-o', device_file])

  if args.out.endswith('/') or os.path.isdir(args.out):
    host_dir = args.out
    host_file = os.path.join(args.out, fname)
  else:
    host_file = args.out
    host_dir = os.path.dirname(host_file)
    if host_dir == '':
      host_dir = '.'
      host_file = './' + host_file
  if not os.path.exists(host_dir):
    shutil.os.makedirs(host_dir)

  if print_log: log_msg("开始抓取 trace...", ANSI.GREEN, "[INFO]")
  
  # Run Perfetto
  proc = adb('shell', *cmd, stdout=subprocess.PIPE)
  proc_out = proc.communicate()[0].decode().strip()
  
  match = re.search(r'^(\d+)$', proc_out, re.M)
  if match is None:
    log_msg('Failed to read the pid from perfetto --background', ANSI.RED, "[ERROR]")
    print(proc_out)
    sys.exit(1)
  bg_pid = match.group(1)
  exit_code = proc.wait()

  if exit_code != 0:
    log_msg('Perfetto 权限不足或调用失败', ANSI.RED, "[ERROR]")
    sys.exit(1)

  # Interactive Live Timer Thread setup
  stop_event = threading.Event()
  def wait_for_stop():
      try:
          input() # Wait for user to press Enter
      except EOFError:
          pass
      stop_event.set()

  threading.Thread(target=wait_for_stop, daemon=True).start()
  
  start_time = time.time()
  adb_failure_count = 0
  ctrl_c_count = 0

  while not stop_event.is_set():
    try:
      poll = adb('shell', f'test -d /proc/{bg_pid} && echo RUN || echo TERM', stdout=subprocess.PIPE)
      poll_res = poll.communicate()[0].decode().strip()
      if poll_res == 'TERM':
        break 
      if poll_res == 'RUN':
        if adb_failure_count > 0:
          adb_failure_count = 0
          print()
          log_msg('ADB connection re-established', ANSI.BLUE, "[INFO]")
          
      duration = int(time.time() - start_time)
      # 将时间部分加上 ANSI.GREEN 和 ANSI.END
      sys.stdout.write(f"\r{ANSI.BLUE}[INFO]{ANSI.END} Tracing duration: {ANSI.GREEN}{duration}s{ANSI.END} | Press Enter to stop... ")
      sys.stdout.flush()
      
      # Wait a second before updating again, break early if stopped
      if stop_event.wait(1.0):
          break

    except (KeyboardInterrupt, SignalException):
      sig = 'TERM' if ctrl_c_count == 0 else 'KILL'
      ctrl_c_count += 1
      print()
      log_msg(f"手动终止 (SIG{sig})", ANSI.BLACK + ANSI.BG_YELLOW, "[WARN]")
      adb('shell', f'kill -{sig} {bg_pid}').wait()
      break
    except Exception:
      adb_failure_count += 1
      if adb_failure_count >= MAX_ADB_FAILURES:
        print()
        log_msg('ADB 中断,设备掉线', ANSI.RED, "[ERROR]")
        sys.exit(1)
      time.sleep(2)

  print() # Clear line after \r
  
  # Terminate manually if user pressed Enter
  if stop_event.is_set():
      adb('shell', f'kill -TERM {bg_pid} >/dev/null 2>&1').wait()

  if args.reporter_api:
    if print_log:
      log_msg('Waiting a few seconds to allow reporter to copy trace')
    time.sleep(5)
    ret = adb('shell', 'cp /sdcard/Android/data/android.perfetto.cts.reporter/files/* ' + device_file).wait()
    if ret != 0:
      log_msg('Failed to extract reporter trace', ANSI.RED, "[ERROR]")
      sys.exit(1)

  if print_log:
    log_msg(f"正在导出到本地: {host_file}", ANSI.BOLD, "[INFO]")
  
  adb('pull', device_file, host_file).wait()
  adb('shell', 'rm -f ' + device_file).wait()
  adb('shell', 'rm -f ' + remote_config_path).wait() # Clean up pushed config

  # 【优化:成功提示】
  log_msg(f"trace 已保存: {host_file}", ANSI.GREEN, "[SUCCESS]")

  if not args.no_open and print_log:
    log_msg(f"Opening the trace ({host_file}) in the browser", ANSI.END, "[INFO]")
    open_browser = not args.no_open_browser
    open_trace_in_browser(host_file, open_browser, args.origin)

  return host_file


# 【修改优化:主函数增加循环逻辑】
def main():
  args = setup_arguments()
  
  # 支持自动化循环抓取
  for i in range(args.loop):
      if args.loop > 1:
          log_msg(f"=== 开始第 {i+1}/{args.loop} 次循环抓取 ===", ANSI.BOLD, "[INFO]")
      
      # 如果有循环,只在最后一次自动打开浏览器
      print_log_and_open = (i == args.loop - 1)
      if not print_log_and_open: 
          args.no_open = True 
          
      start_trace(args, print_log=True)
      
      if args.loop > 1 and i < args.loop - 1:
          time.sleep(2) # 循环之间的缓冲时间

  # 清理自动生成的临时配置
  if args.scene != 'none' and os.path.exists("auto_generated_scene.pbtx"):
      os.remove("auto_generated_scene.pbtx")

def find_adb():
  global adb_path
  for path in ['adb', HERMETIC_ADB_PATH]:
    try:
      subprocess.call([path, '--version'], stdout=devnull, stderr=devnull)
      adb_path = path
      break
    except OSError:
      continue
  if adb_path is None:
    sdk_url = 'https://developer.android.com/studio/releases/platform-tools'
    log_msg('Could not find a suitable adb binary in the PATH. ', ANSI.RED, "[ERROR]")
    log_msg('You can download adb from %s' % sdk_url, ANSI.RED, "[ERROR]")
    sys.exit(1)


# 【新增优化:重写 TCPServer 错误处理,屏蔽 Ctrl+C 导致的堆栈打印】
class QuietTCPServer(socketserver.TCPServer):
  def handle_error(self, request, client_address):
    # 如果是用户手动中断 (SignalException),则静默处理
    if sys.exc_info()[0] is SignalException:
      pass
    else:
      super().handle_error(request, client_address)

def open_trace_in_browser(path, open_browser, origin):
  PORT = 9001
  path = os.path.abspath(path)
  os.chdir(os.path.dirname(path))
  fname = os.path.basename(path)
  QuietTCPServer.allow_reuse_address = True
  
  try:
    with QuietTCPServer(('127.0.0.1', PORT), HttpHandler) as httpd:
      address = f'{origin}/#!/?url=http://127.0.0.1:{PORT}/{fname}&referrer=record_android_trace'
      if open_browser:
        webbrowser.open_new_tab(address)
      else:
        print(f'Open URL in browser: {address}')

      httpd.expected_fname = fname
      httpd.fname_get_completed = None
      httpd.allow_origin = origin
      
      # 捕获并等待浏览器请求
      while httpd.fname_get_completed is None:
        httpd.handle_request()
        
  except SignalException:
    # 捕获 Ctrl+C 并平滑退出本地服务
    print("\n")
    log_msg("本地 UI 代理服务已安全关闭。", ANSI.BLUE, "[INFO]")


def adb(*args, stdin=devnull, stdout=None, stderr=None):
  cmd = [adb_path, *args]
  setpgrp = None
  if os.name != 'nt':
    setpgrp = lambda: os.setpgrp()
  proc = subprocess.Popen(
      cmd, stdin=stdin, stdout=stdout, stderr=stderr, preexec_fn=setpgrp)
  procs.append(proc)
  return proc


def kill_all_subprocs_on_exit():
  for p in [p for p in procs if p.poll() is None]:
    p.kill()

if __name__ == '__main__':
  sys.exit(main())

配置文件:syshealth_google.pbtx

bash 复制代码
# go/syshealth.pbtx
# Canonical trace config for Android System Health reviews.
# See go/android-system-health-faq#instructions-for-taking-a-perfetto-trace.
# Buffer 0: For most continuous data (ftrace, power, sys_stats, frametimeline)
buffers: {
    size_kb: 260096
    fill_policy: RING_BUFFER
}
# Buffer 1: For process_stats
buffers: {
    size_kb: 2048
    fill_policy: RING_BUFFER
}
# Buffer 2: For logging
buffers: {
    size_kb: 2048
    fill_policy: RING_BUFFER
}
# Buffer 3: For packages list and system info
buffers: {
    size_kb: 1024
    fill_policy: RING_BUFFER
}
# Buffer 4: Stastd atoms
buffers: {
    size_kb: 256
    fill_policy: RING_BUFFER
}
data_sources {
  config {
    name: "track_event"
    track_event_config {
      disabled_categories:"*"
      enabled_categories: "servicemanager"
    }
  }
}
data_sources: {
    config {
        name: "linux.process_stats"
        target_buffer: 1
        process_stats_config {
            scan_all_processes_on_start: true
            # As we're primarily interested in memory stats, we don't need to
            # poll very often as we also have the kmem/rss_stat ftrace event.
            proc_stats_poll_ms: 10000
        }
    }
}
data_sources: {
    config {
        name: "linux.sys_stats"
        target_buffer: 0
        sys_stats_config {
            #psi_period_ms: 100
        }
    }
}
data_sources: {
    config {
        name: "linux.system_info"
        target_buffer: 3
    }
}
data_sources: {
    config {
        name: "android.surfaceflinger.frametimeline"
        target_buffer: 0
    }
}
data_sources: {
    config {
        name: "android.power"
        target_buffer: 0
        android_power_config {
            battery_poll_ms: 500
            collect_power_rails: true
        }
    }
}
data_sources: {
    config {
        name: "android.log"
        target_buffer: 2
        android_log_config {
            log_ids: LID_DEFAULT
        }
    }
}
data_sources: {
    config {
        name: "android.packages_list"
        target_buffer: 3
    }
}
data_sources: {
    config {
        name: "android.system_property"
        target_buffer: 3
        android_system_property_config {
            # No need to poll often, as boot image profiling properties are set
            # once during the boot sequence.
            poll_ms: 30000
            property_name: "debug.tracing.profile_boot_classpath"
            property_name: "debug.tracing.profile_system_server"
        }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        target_buffer: 0
        ftrace_config {
            ftrace_events: "sched/sched_switch"
            ftrace_events: "power/suspend_resume"
            ftrace_events: "sched/sched_wakeup_new"
            ftrace_events: "sched/sched_waking"
            ftrace_events: "power/cpu_frequency"
            ftrace_events: "power/cpu_idle"
            ftrace_events: "mm_event/mm_event_record"
            ftrace_events: "kmem/rss_stat"
            ftrace_events: "ion/ion_stat"
            ftrace_events: "dmabuf_heap/dma_heap_stat"
            ftrace_events: "kmem/ion_heap_grow"
            ftrace_events: "kmem/ion_heap_shrink"
            ftrace_events: "sched/sched_process_exit"
            ftrace_events: "sched/sched_process_free"
            ftrace_events: "task/task_newtask"
            ftrace_events: "task/task_rename"
            ftrace_events: "sched/sched_blocked_reason"
            ftrace_events: "workqueue/*"
            ftrace_events: "vmscan/*"
            ftrace_events: "ftrace/print"
            ftrace_events: "power/wakeup_source_activate"
            ftrace_events: "power/wakeup_source_deactivate"
            atrace_categories: "aidl"
            atrace_categories: "am"
            atrace_categories: "dalvik"
            atrace_categories: "binder_driver"
            atrace_categories: "bionic"
            atrace_categories: "gfx"
            atrace_categories: "hal"
            atrace_categories: "input"
            atrace_categories: "pm"
            atrace_categories: "power"
            atrace_categories: "res"
            atrace_categories: "sm"
            atrace_categories: "ss"
            atrace_categories: "video"
            atrace_categories: "view"
            atrace_categories: "wm"
			atrace_categories: "disk"
            atrace_apps: "lmkd"
            atrace_apps: "*"
            buffer_size_kb: 16384
            drain_period_ms: 250
            throttle_rss_stat: true
            symbolize_ksyms: true
            ksyms_mem_policy: KSYMS_RETAIN
            disable_generic_events: true
            compact_sched { enabled: true }
        }
    }
}
data_sources: {
    config {
        name: "android.statsd"
        target_buffer: 4
        statsd_tracing_config {
            push_atom_id: ATOM_SCHEDULED_JOB_STATE_CHANGED
        }
    }
}
duration_ms: 1800000
write_into_file: true
file_write_period_ms: 5000
max_file_size_bytes: 1000000000
flush_period_ms: 30000
incremental_state_config {
    clear_period_ms: 5000
}
相关推荐
黄林晴2 小时前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter
Pyeako2 小时前
基于Qt和PaddleOCR的工业视觉识别报警系统开发
人工智能·python·深度学习·数码相机·opencv·ocr·pyqt5
Kapaseker2 小时前
一杯 Kotlin 美式品味 object 声明
android·kotlin
常利兵2 小时前
Room 3.0大变身:安卓开发的新挑战与机遇
android·jvm·oracle
未知鱼2 小时前
Python安全开发asyncio(异步IO与高并发)
python·安全·网络安全·github
代码探秘者2 小时前
【大模型应用】5.深入理解向量数据库
java·数据库·后端·python·spring·面试
小鸡吃米…2 小时前
Python 网络爬虫
开发语言·爬虫·python
阿拉斯攀登2 小时前
【RK3576 安卓 JNI/NDK 系列 09】RK3576 实战(三):JNI 调用 librga 实现 2D 硬件加速图像处理
android·驱动开发·rk3568·瑞芯微·rk安卓驱动·rk3576 rga加速
2401_832035342 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python