[iOS] CPU 특정 시간대 평균 성능 측정하기

유인호·2025년 7월 17일
0

iOS

목록 보기
85/85

0. 서론

겉멋을 위해 CPU의 평균 Usage를 측정해보려고 Instrument를 켰다. 하지만 적어도 내가 확인했을때는 Xcode Instrument엔 해당 시나리오(시간대)의 평균 사용량을 알려주지는 않고 있었다.

그렇다면 나는 겉멋을 위해 이렇게 포기해야만 하는가...???

1. 탐색

첫번째로 시도해본것은 Instrument에 10ms단위로 있는 녀석들을 하나하나 계산기로 더해서 하는 방식이다.
그러나 CPU사용량이 Zero일때는 아에 Record가 되지 않는다..!!! 그래서 키보드로 > 눌러서 확인해보았을때 건너뛰어지는 부분이 생겼음.
이것 떄문에 정확한 측정은 어려울 것이라고 생각함.

이전

이후 (많이 좋아지긴 했져?)

아무튼 사진으로는 보여줄 수 있는데 겉멋의 끝은 수치 아니겠는가, 나는 수치를 뽑고 싶었다.

2. 방법

Instrument자체에는 아무리 봐도 뽑아낼 방법이 없었다. (못 찾았을 수도 있지만) 그래서 묘수를 하나 생각해냄.

Xcode Instrument 결과는 저장할 수 있다는것을 떠올렸음. .trace 확장자로 저장할 수 있는데, 결과를 저장한다는건 그 안에 데이터도 존재한다는것 아닌가!!

딱 이 생각이 들자마자 Cursor와 GPT를 이용해서 .trace파일을 분석하고 해체할 수 있는 방법을 찾았음.
참고로 이 포스팅의 기준은 Instrument의 CPU 쪽 분석 기준임

xctrace export \
  --input YourTrace.trace \
  --xpath '/trace-toc/run[@number="1"]/data/table[@schema="cpu-profile"]' \
  --output cpu_profile.xml

지피티한테 물어보니 .trace를 해체 할 수 있는 이런 명령어가 있었다.

ㅇㅋ 좋습니다.

xml로 데이터를 뽑아냈으니 사실 여기서 거의 다 했고, Cursor에게 cpu_profile.xml를 보고 데이터 뽑아내는 파이썬 코드 하나 짜달라고 했음.

파일이 너무크다고 머라고 했지만 그래도 열심히 분석해줬다.

처음에는 Cursor도 %단위로 뽑아보려고 노력해본것 같은데, 이게 %가 아닌 애플에서 만든 어떤 공식같은걸로 변환해놓은 듯한 수치가 있어서 정확한 rawvalue를 뽑아내보려고 노력했지만 리버스 엔지니어링은 조금 어려웠음.

그래서 %는 포기하고 그냥 평균만 내자! 해서 평균을 냈는데 이게 웬걸, 사진에서 본것과 다르게 50%정도 밖에 최적화를 못시켰다고 계산을 해줬음.

이 문제는 아까 말했듯 0%는 애초에 record 자체를 안해서 평균을 내버리면 문제가 생겨버리는 것 이였다.

3. 결과

평균에 문제가 있었기 때문에 개선이 안된 버전과 개선된 버전의 각각의 버전에서 내가 측정을 원하는 시나리오에 겹치는 시간을 찾고, 그 사이에 있는 모든 값들을 더해서 개선률을 구해보도록 수정했다.

결론적으로 96.86% 최적화 했다는것을 얻어낼 수 있었다.

4. 사용된 코드 (Made by Cursor)

cpu_usage_parser.py

import xml.etree.ElementTree as ET
import re
from typing import Optional, List, Tuple

def parse_time_str(time_str: str) -> float:
    """
    Parse time string like "00:00.507.180" to seconds as float
    Format: MM:SS.mmm.uuu (minutes:seconds.milliseconds.microseconds)
    """
    # Remove any whitespace
    time_str = time_str.strip()
    
    # Pattern: MM:SS.mmm.uuu
    pattern = r'^(\d{2}):(\d{2})\.(\d{3})\.(\d{3})$'
    match = re.match(pattern, time_str)
    
    if not match:
        raise ValueError(f"Invalid time format: {time_str}")
    
    minutes, seconds, milliseconds, microseconds = match.groups()
    
    # Convert to total seconds
    total_seconds = (
        int(minutes) * 60 +
        int(seconds) +
        int(milliseconds) / 1000.0 +
        int(microseconds) / 1000000.0
    )
    
    return total_seconds

def parse_cpu_value(cpu_str: str) -> float:
    """
    Parse CPU cycle weight string like "434.97 Kc" to numeric value
    """
    # Remove any whitespace
    cpu_str = cpu_str.strip()
    
    # Extract numeric part (ignore units like "Kc", "Mc", etc.)
    match = re.match(r'^([\d.]+)', cpu_str)
    if match:
        value = float(match.group(1))
        
        # Handle units (Kc = kilo cycles, Mc = mega cycles, etc.)
        if 'Mc' in cpu_str:
            value *= 1000  # Convert mega to kilo
        elif 'Kc' in cpu_str:
            value *= 1     # Already in kilo
        elif cpu_str == '-' or cpu_str == '0':
            value = 0
            
        return value
    
    return 0.0

def sum_cpu_usage_in_range(
    xml_file: str,
    start_time_str: str,
    end_time_str: str,
    timestamp_tag: str = 'sample-time',
    cpu_tag: str = 'cycle-weight'
) -> Optional[float]:
    """
    Calculate sum of CPU usage in the specified time range from xctrace XML export
    
    Args:
        xml_file: Path to the XML file
        start_time_str: Start time in format "MM:SS" (e.g., "00:30")
        end_time_str: End time in format "MM:SS" (e.g., "01:00")
        timestamp_tag: XML tag name for timestamp (default: 'sample-time')
        cpu_tag: XML tag name for CPU usage (default: 'cycle-weight')
    
    Returns:
        Sum of CPU usage as float, or None if no data found
    """
    try:
        # Parse the XML file
        tree = ET.parse(xml_file)
        root = tree.getroot()
        
        # Convert start and end times to seconds
        start_seconds = parse_time_str(start_time_str + ".000.000")
        end_seconds = parse_time_str(end_time_str + ".000.000")
        
        cpu_values = []
        
        # Find all rows in the XML
        for row in root.iter('row'):
            # Find timestamp and CPU usage elements
            timestamp_elem = row.find(timestamp_tag)
            cpu_elem = row.find(cpu_tag)
            
            if timestamp_elem is not None and cpu_elem is not None:
                # Parse timestamp
                time_fmt = timestamp_elem.get('fmt')
                if time_fmt:
                    try:
                        time_seconds = parse_time_str(time_fmt)
                        
                        # Check if time is within range
                        if start_seconds <= time_seconds <= end_seconds:
                            # Parse CPU value
                            cpu_fmt = cpu_elem.get('fmt')
                            if cpu_fmt and cpu_fmt != '-':
                                cpu_value = parse_cpu_value(cpu_fmt)
                                cpu_values.append(cpu_value)
                    except ValueError:
                        # Skip invalid time formats
                        continue
        
        # Calculate sum
        if cpu_values:
            return sum(cpu_values)
        else:
            return None
            
    except Exception as e:
        print(f"Error parsing XML: {e}")
        return None

def average_cpu_usage_in_range(
    xml_file: str,
    start_time_str: str,
    end_time_str: str,
    timestamp_tag: str = 'sample-time',
    cpu_tag: str = 'cycle-weight'
) -> Optional[float]:
    """
    Calculate average CPU usage in the specified time range from xctrace XML export
    
    Args:
        xml_file: Path to the XML file
        start_time_str: Start time in format "MM:SS" (e.g., "00:30")
        end_time_str: End time in format "MM:SS" (e.g., "01:00")
        timestamp_tag: XML tag name for timestamp (default: 'sample-time')
        cpu_tag: XML tag name for CPU usage (default: 'cycle-weight')
    
    Returns:
        Average CPU usage as float, or None if no data found
    """
    try:
        # Parse the XML file
        tree = ET.parse(xml_file)
        root = tree.getroot()
        
        # Convert start and end times to seconds
        start_seconds = parse_time_str(start_time_str + ".000.000")
        end_seconds = parse_time_str(end_time_str + ".000.000")
        
        cpu_values = []
        
        # Find all rows in the XML
        for row in root.iter('row'):
            # Find timestamp and CPU usage elements
            timestamp_elem = row.find(timestamp_tag)
            cpu_elem = row.find(cpu_tag)
            
            if timestamp_elem is not None and cpu_elem is not None:
                # Parse timestamp
                time_fmt = timestamp_elem.get('fmt')
                if time_fmt:
                    try:
                        time_seconds = parse_time_str(time_fmt)
                        
                        # Check if time is within range
                        if start_seconds <= time_seconds <= end_seconds:
                            # Parse CPU value
                            cpu_fmt = cpu_elem.get('fmt')
                            if cpu_fmt and cpu_fmt != '-':
                                cpu_value = parse_cpu_value(cpu_fmt)
                                cpu_values.append(cpu_value)
                    except ValueError:
                        # Skip invalid time formats
                        continue
        
        # Calculate average
        if cpu_values:
            return sum(cpu_values) / len(cpu_values)
        else:
            return None
            
    except Exception as e:
        print(f"Error parsing XML: {e}")
        return None

def get_time_range_info(xml_file: str) -> Tuple[str, str]:
    """
    Get the time range available in the XML file
    
    Returns:
        Tuple of (start_time, end_time) in format "MM:SS.mmm.uuu"
    """
    try:
        tree = ET.parse(xml_file)
        root = tree.getroot()
        
        times = []
        for row in root.iter('row'):
            timestamp_elem = row.find('sample-time')
            if timestamp_elem is not None:
                time_fmt = timestamp_elem.get('fmt')
                if time_fmt:
                    times.append(time_fmt)
        
        if times:
            return times[0], times[-1]
        else:
            return "N/A", "N/A"
            
    except Exception as e:
        print(f"Error getting time range: {e}")
        return "N/A", "N/A"

if __name__ == "__main__":
    # Test the parser
    xml_file = "cpu_profile1.xml"
    
    # Get time range info
    start_time, end_time = get_time_range_info(xml_file)
    print(f"Available time range: {start_time} to {end_time}")
    
    # Test with a small range
    avg = average_cpu_usage_in_range(
        xml_file=xml_file,
        start_time_str='00:00',
        end_time_str='00:01'
    )
    
    if avg is not None:
        print(f"Average CPU usage (00:00-00:01): {avg:.2f} Kc")
    else:
        print("No data found in the specified range") 

get_average.py

from cpu_usage_parser import sum_cpu_usage_in_range

total = sum_cpu_usage_in_range(
    xml_file='cpu_profile1.xml',
    start_time_str='00:12',   
    end_time_str='02:12',   
    timestamp_tag='sample-time', # 실제 XML 태그명
    cpu_tag='cycle-weight'       # 실제 XML 태그명
)
if total is not None:
    print(f"Total cycle-weight: {total:.2f} Kc")
else:
    print("No data in the specified range.")
profile
🍎Apple Developer Academy @ POSTECH 2nd, 🌱SeSAC iOS 4th

0개의 댓글