blob: c5f03460d1512acb6aaa515608e53837eb693d20 [file] [log] [blame]
#!/usr/bin/env python3
import glob
import logging
import math
import os
import shlex
import shutil
import subprocess
from functools import cache
logger = logging.getLogger(__name__)
def locate_binary_xcrun(sdk, binary_name):
completed_process = subprocess.run(['/usr/bin/xcrun', '-sdk', sdk, '--find', binary_name],
check=False, text=True, capture_output=True)
if completed_process.returncode:
return None
return completed_process.stdout.strip()
def simplify_profile_weights(profile_weights):
simplified_profile_weights = []
weight_sum = 0
max_weight = 0
# We need to turn percentages into weights > 1, but we don't want crazy high multipliers.
# For example, if we have weights 0.35 and 0.65, we don't need a 7:13 ratio when 5:9 is good enough.
max_multiplier = 15
for group, weight in profile_weights:
weight_sum = weight_sum + weight
if weight > max_weight:
max_weight = weight
gcd = int(max_weight * max_multiplier)
for group, weight in profile_weights:
gcd = math.gcd(gcd, int((weight / weight_sum) * max_multiplier))
for i in range(0, len(profile_weights)):
group, weight = profile_weights[i]
simplified_profile_weights.append((group, int((weight / weight_sum) * max_multiplier) // gcd))
return simplified_profile_weights
class ExecutablesFromEnvAndXcode:
PREFERRED_EXECUTABLE_INDEX = 0
EXECUTABLE_NAME = None
@classmethod
@cache
def detect_binaries(cls):
llvm_profdata_binaries = []
llvm_profdata_from_search_path = shutil.which(cls.EXECUTABLE_NAME)
if llvm_profdata_from_search_path:
llvm_profdata_binaries.append(llvm_profdata_from_search_path)
for sdk_name in ('macosx.internal', 'iphoneos.internal', 'macosx', 'iphoneos'):
binary_path = locate_binary_xcrun(sdk_name, cls.EXECUTABLE_NAME)
if not binary_path:
continue
if binary_path in llvm_profdata_binaries:
continue
llvm_profdata_binaries.append(binary_path)
logger.debug(f'Available {cls.EXECUTABLE_NAME} from {llvm_profdata_binaries}')
return llvm_profdata_binaries
@classmethod
def preference_ordered_paths(cls):
count = len(cls.detect_binaries())
for _ in range(count):
cls.PREFERRED_EXECUTABLE_INDEX = (cls.PREFERRED_EXECUTABLE_INDEX + 1) % count
yield cls.detect_binaries()[cls.PREFERRED_EXECUTABLE_INDEX]
@classmethod
def run(cls, command, *args, check=False, stdout=None, stderr=None, capture_output=False,
**kwargs) -> subprocess.CompletedProcess:
kwarg_capture_output = capture_output or (stdout is None and stderr is None)
completed_process = None
for binary_path in cls.preference_ordered_paths():
logger.debug(f'Running {shlex.join([binary_path, *command])}')
completed_process = subprocess.run([binary_path, *command], *args,
check=False, capture_output=kwarg_capture_output,
stdout=stdout, stderr=stderr, **kwargs)
if not completed_process.returncode:
break
logger.debug(f'Failed to {command} with binary {binary_path}\n'
f'return_code: {completed_process.returncode}\n'
f'stdout: {completed_process.stdout}\n'
f'stderr: {completed_process.stderr}\n')
if check:
completed_process.check_returncode()
return completed_process
class LLVMProfDataExecutable(ExecutablesFromEnvAndXcode):
EXECUTABLE_NAME = 'llvm-profdata'
class LLVMProfileData:
@classmethod
def show(cls, profile_path):
list_functions_process = LLVMProfDataExecutable.run(['show', '--all-functions', '--value-cutoff=10',
profile_path], stdout=subprocess.PIPE, text=True)
return subprocess.run(['/usr/bin/c++filt', '-n'], input=list_functions_process.stdout,
capture_output=True, text=True, check=True)
@classmethod
def merge(cls, output_file, unweighted_profiles=(), weighted_profiles=()):
command = ['merge', '--sparse', *unweighted_profiles]
for profile_path, weight in weighted_profiles:
lib_profile_path = profile_path
command.extend(['--weighted-input', f'{weight},{lib_profile_path}'])
command.extend(['--output', output_file])
return LLVMProfDataExecutable.run(command, capture_output=True, text=True)
@classmethod
def compress(cls, input_profile, output_file):
return subprocess.run(['/usr/bin/compression_tool', '-encode', '-i', input_profile, '-o', output_file,
'-a', 'lzfse'], capture_output=True, check=True, text=True)
def merge_raw_profiles_in_directory_by_prefixes(prefix_list, input_directory, output_directory=None,
input_suffix='.profraw', output_suffix='.profdata'):
output_files = []
for prefix in prefix_list:
logger.info(f'Merging {prefix}')
pattern = f'{prefix}*{input_suffix}'
input_profiles = glob.glob(os.path.join(input_directory, pattern))
output_file = os.path.join(output_directory or input_directory, f'{prefix}{output_suffix}')
merge_process = LLVMProfileData.merge(output_file, unweighted_profiles=input_profiles)
logger.info(f'stdout: {merge_process.stdout}')
logger.info(f'stderr: {merge_process.stderr}')
merge_process.check_returncode()
output_files.append(output_file)
logger.info(f'{prefix} is successfully merged')
return output_files