Repository navigation
[Task 1/5] [No QA] Add native PGO build and benchmark tooling #97808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3fa458a
3295d38
e1747b5
96518ea
27c7035
e8d1104
2b114fd
9322dba
47f9d5b
6ba45d6
62808e8
0da014a
ea4b7fb
151f4f1
1921255
13df28a
f906d1a
7bf73a0
4ce4248
c267adb
43425ff
aa96634
dfceb96
e25759f
04f20ae
7f56cfb
c68accc
fa69456
5ddd179
22a19cd
d35b7f5
b143c0f
9befe34
169f09f
66a65c1
f44192d
d94d768
2beedd0
72c97bd
c4bd27d
6aa1031
f6978f1
a9cae54
394ac25
b7b4105
7673df9
21f2d6c
e83fa48
d0cfd6a
f9b823c
e2cf575
13943b7
69a8d67
83c7263
e9dec6c
0e6f454
74f329c
16db894
4cce518
6aab15b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.expensify.chat; | ||
|
|
||
| import android.content.BroadcastReceiver; | ||
| import android.content.Context; | ||
| import android.content.Intent; | ||
| import android.util.Log; | ||
|
|
||
| /** | ||
| * Receives an explicit adb broadcast at the end of a local PGO collection run. | ||
| * | ||
| * The receiver is inert in ordinary builds. It deliberately lives outside the JS surface so a | ||
| * manual profiling journey can finish before its native counters are persisted. | ||
| */ | ||
| public final class PgoProfileReceiver extends BroadcastReceiver { | ||
| private static final String TAG = "PgoProfileReceiver"; | ||
|
|
||
| @Override | ||
| public void onReceive(Context context, Intent intent) { | ||
| if (!BuildConfig.PGO_PROFILE_GENERATION) { | ||
| Log.w(TAG, "Ignoring PGO profile request in a non-instrumented build."); | ||
| return; | ||
| } | ||
|
|
||
| final int writtenProfiles = PgoProfileWriter.writeProfiles(context); | ||
| Log.i(TAG, "Wrote " + writtenProfiles + " LLVM PGO profile(s)."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.expensify.chat; | ||
|
|
||
| import android.content.Context; | ||
|
|
||
| /** Development-only bridge for persisting LLVM PGO counters before Android kills the process. */ | ||
| public final class PgoProfileWriter { | ||
| private PgoProfileWriter() {} | ||
|
|
||
| private static native int writeProfiles(String directory); | ||
|
|
||
| public static int writeProfiles(Context context) { | ||
| if (!BuildConfig.PGO_PROFILE_GENERATION) { | ||
| return 0; | ||
| } | ||
|
|
||
| System.loadLibrary("ExpensifyNitroUtils"); | ||
| return writeProfiles(context.getCacheDir().getAbsolutePath()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Vit's agent) The PGO scripts only target HybridApp ( |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
|
|
||
| + (void)initWithStoryboard:(NSString * _Nonnull)storyboardName | ||
| rootView:(UIView * _Nullable)rootView; | ||
| + (void)initializePGOProfileCollection; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Vit's agent) Nit: this puts about 100 lines of profiling code in the boot splash module. A small |
||
| + (void)hide:(BOOL)fade; | ||
| + (void)bringSubviewToFrontIfInitialized; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,13 +13,62 @@ | |
| #import <React/RCTSurfaceHostingView.h> | ||
| #import <React/RCTRootView.h> | ||
|
|
||
| #ifndef EXPENSIFY_PGO_GENERATE | ||
| #define EXPENSIFY_PGO_GENERATE 0 | ||
| #endif | ||
|
|
||
| #if EXPENSIFY_PGO_GENERATE | ||
| #include <dlfcn.h> | ||
|
|
||
| extern "C" void __llvm_profile_set_filename(const char *FilenamePat); | ||
| extern "C" int __llvm_profile_write_file(void); | ||
| extern "C" void __llvm_profile_reset_counters(void); | ||
|
|
||
| using PGOSetFilename = void (*)(const char *); | ||
| using PGOWriteFile = int (*)(); | ||
| using PGOResetCounters = void (*)(); | ||
|
|
||
| template <typename Function> | ||
| static Function HermesPGOFunction(const char *symbol) { | ||
| return reinterpret_cast<Function>(dlsym(RTLD_DEFAULT, symbol)); | ||
| } | ||
| #endif | ||
|
|
||
| static NSString *const PGO_DIRECTORY_NAME = @"ExpensifyPGO"; | ||
| static NSString *const PGO_STATUS_MARKER = @"profile-status.txt"; | ||
| static CFStringRef const PGO_WRITE_NOTIFICATION = CFSTR("com.expensify.pgo.write-profiles"); | ||
| static CFStringRef const PGO_CLEAR_NOTIFICATION = CFSTR("com.expensify.pgo.clear-profiles"); | ||
|
|
||
| static RCTSurfaceHostingProxyRootView *_rootView = nil; | ||
|
|
||
| static UIView *_loadingView = nil; | ||
| static NSMutableArray<RCTPromiseResolveBlock> *_resolveQueue = [[NSMutableArray alloc] init]; | ||
| static bool _fade = false; | ||
| static bool _nativeHidden = false; | ||
|
|
||
| static long long CurrentTimeMilliseconds(void) { | ||
| return (long long)([[NSDate date] timeIntervalSince1970] * 1000); | ||
| } | ||
|
|
||
| @interface RCTBootSplash () | ||
| + (NSURL *)pgoDirectoryURL; | ||
| + (void)writePGOStatus:(NSString *)status result:(int)result; | ||
| + (void)writePGOProfiles; | ||
| + (void)clearPGOProfiles; | ||
| @end | ||
|
|
||
| static void HandlePGONotification(__unused CFNotificationCenterRef center, | ||
| __unused void *observer, | ||
| CFStringRef name, | ||
| __unused const void *object, | ||
| __unused CFDictionaryRef userInfo) { | ||
| if (CFEqual(name, PGO_WRITE_NOTIFICATION)) { | ||
| [RCTBootSplash writePGOProfiles]; | ||
| } else if (CFEqual(name, PGO_CLEAR_NOTIFICATION)) { | ||
| [RCTBootSplash clearPGOProfiles]; | ||
| } | ||
| } | ||
|
|
||
| @implementation RCTBootSplash | ||
|
|
||
| RCT_EXPORT_MODULE(); | ||
|
|
@@ -32,6 +81,81 @@ + (BOOL)requiresMainQueueSetup { | |
| return NO; | ||
| } | ||
|
|
||
| + (NSURL *)pgoDirectoryURL { | ||
| NSURL *cachesURL = [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory | ||
| inDomains:NSUserDomainMask] firstObject]; | ||
| NSURL *directoryURL = [cachesURL URLByAppendingPathComponent:PGO_DIRECTORY_NAME isDirectory:YES]; | ||
| [[NSFileManager defaultManager] createDirectoryAtURL:directoryURL | ||
| withIntermediateDirectories:YES | ||
| attributes:nil | ||
| error:nil]; | ||
| return directoryURL; | ||
| } | ||
|
|
||
| + (void)writePGOStatus:(NSString *)status result:(int)result { | ||
| NSString *contents = [NSString stringWithFormat:@"%lld,%@,%d", CurrentTimeMilliseconds(), status, result]; | ||
| NSURL *statusURL = [[self pgoDirectoryURL] URLByAppendingPathComponent:PGO_STATUS_MARKER]; | ||
| [contents writeToURL:statusURL atomically:YES encoding:NSUTF8StringEncoding error:nil]; | ||
| } | ||
|
|
||
| + (void)initializePGOProfileCollection { | ||
| static dispatch_once_t onceToken; | ||
| dispatch_once(&onceToken, ^{ | ||
| CFNotificationCenterRef notificationCenter = CFNotificationCenterGetDarwinNotifyCenter(); | ||
| CFNotificationCenterAddObserver(notificationCenter, NULL, HandlePGONotification, PGO_WRITE_NOTIFICATION, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); | ||
| CFNotificationCenterAddObserver(notificationCenter, NULL, HandlePGONotification, PGO_CLEAR_NOTIFICATION, NULL, CFNotificationSuspensionBehaviorDeliverImmediately); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notifications are registered even when EXPENSIFY_PGO_GENERATE is disabled.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seconding this |
||
|
|
||
| #if EXPENSIFY_PGO_GENERATE | ||
| NSString *profilePattern = [[[self pgoDirectoryURL] URLByAppendingPathComponent:@"newdot-%m.profraw"] path]; | ||
| __llvm_profile_set_filename(profilePattern.fileSystemRepresentation); | ||
| PGOSetFilename setHermesFilename = HermesPGOFunction<PGOSetFilename>("expensify_llvm_profile_set_filename"); | ||
| if (setHermesFilename != nullptr) { | ||
| setHermesFilename(profilePattern.fileSystemRepresentation); | ||
| } | ||
| #endif | ||
| }); | ||
| } | ||
|
|
||
| + (void)writePGOProfiles { | ||
| #if EXPENSIFY_PGO_GENERATE | ||
| int appResult = __llvm_profile_write_file(); | ||
| PGOWriteFile writeHermesFile = HermesPGOFunction<PGOWriteFile>("expensify_llvm_profile_write_file"); | ||
| int hermesResult = writeHermesFile != nullptr ? writeHermesFile() : 1; | ||
| int result = appResult == 0 && hermesResult == 0 ? 0 : 1; | ||
| [self writePGOStatus:@"written" result:result]; | ||
| NSLog(@"ExpensifyPGO: wrote LLVM profile with result=%d appResult=%d hermesResult=%d", result, appResult, hermesResult); | ||
| #else | ||
| [self writePGOStatus:@"not-instrumented" result:1]; | ||
| NSLog(@"ExpensifyPGO: ignored profile write in a non-instrumented build"); | ||
| #endif | ||
| } | ||
|
|
||
| + (void)clearPGOProfiles { | ||
| NSFileManager *fileManager = [NSFileManager defaultManager]; | ||
| NSURL *directoryURL = [self pgoDirectoryURL]; | ||
| NSArray<NSURL *> *files = [fileManager contentsOfDirectoryAtURL:directoryURL | ||
| includingPropertiesForKeys:nil | ||
| options:0 | ||
| error:nil]; | ||
| for (NSURL *fileURL in files) { | ||
| if ([fileURL.lastPathComponent hasSuffix:@".profraw"] || [fileURL.lastPathComponent isEqualToString:PGO_STATUS_MARKER]) { | ||
| [fileManager removeItemAtURL:fileURL error:nil]; | ||
| } | ||
| } | ||
|
|
||
| #if EXPENSIFY_PGO_GENERATE | ||
| __llvm_profile_reset_counters(); | ||
| PGOResetCounters resetHermesCounters = HermesPGOFunction<PGOResetCounters>("expensify_llvm_profile_reset_counters"); | ||
| int result = resetHermesCounters != nullptr ? 0 : 1; | ||
| if (resetHermesCounters != nullptr) { | ||
| resetHermesCounters(); | ||
| } | ||
| [self writePGOStatus:@"cleared" result:result]; | ||
| #else | ||
| [self writePGOStatus:@"not-instrumented" result:1]; | ||
| #endif | ||
| } | ||
|
|
||
| + (bool)isLoadingViewVisible { | ||
| return _loadingView != nil && ![_loadingView isHidden]; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,42 @@ | ||
| project(ExpensifyNitroUtils) | ||
| # cspell:ignore STREQUAL libclang fprofile defsym | ||
|
|
||
| cmake_minimum_required(VERSION 3.9.0) | ||
|
|
||
| set(PACKAGE_NAME ExpensifyNitroUtils) | ||
| set(CMAKE_VERBOSE_MAKEFILE ON) | ||
| set(CMAKE_CXX_STANDARD 20) | ||
|
|
||
| if(EXPENSIFY_PGO_MODE STREQUAL "generate") | ||
| execute_process( | ||
| COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir | ||
| OUTPUT_VARIABLE CLANG_RESOURCE_DIR | ||
| OUTPUT_STRIP_TRAILING_WHITESPACE | ||
| ) | ||
| set(PGO_RUNTIME "${CLANG_RESOURCE_DIR}/lib/linux/libclang_rt.profile-aarch64-android.a") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Vit's agent) This hardcodes the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if(NOT EXISTS "${PGO_RUNTIME}") | ||
| message(FATAL_ERROR "LLVM profiling runtime was not found at ${PGO_RUNTIME}") | ||
| endif() | ||
| add_compile_options(-fprofile-generate) | ||
| add_link_options( | ||
| -fprofile-generate | ||
| "-Wl,--whole-archive,${PGO_RUNTIME},--no-whole-archive" | ||
| -Wl,--undefined=__llvm_profile_set_filename | ||
| -Wl,--undefined=__llvm_profile_write_file | ||
| -Wl,--defsym=expensify_llvm_profile_set_filename=__llvm_profile_set_filename | ||
| -Wl,--defsym=expensify_llvm_profile_write_file=__llvm_profile_write_file | ||
| -Wl,--export-dynamic-symbol=expensify_llvm_profile_set_filename | ||
| -Wl,--export-dynamic-symbol=expensify_llvm_profile_write_file | ||
| ) | ||
| elseif(EXPENSIFY_PGO_MODE STREQUAL "use") | ||
| add_compile_options("-fprofile-use=${EXPENSIFY_PGO_PROFILE}") | ||
| add_link_options("-fprofile-use=${EXPENSIFY_PGO_PROFILE}") | ||
| endif() | ||
|
|
||
| # Define C++ library and add all sources | ||
| add_library(${PACKAGE_NAME} SHARED | ||
| src/main/cpp/cpp-adapter.cpp | ||
| src/main/cpp/PgoProfileWriter.cpp | ||
| ) | ||
|
|
||
| # Add Nitrogen specs | ||
|
|
@@ -29,4 +58,4 @@ target_link_libraries( | |
| ${PACKAGE_NAME} | ||
| ${LOG_LIB} | ||
| android | ||
| ) | ||
| ) | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Vit's agent) This receiver ships in production with no permission, so any app on the device can send it. One line locks it to adb: the shell user holds
DUMP, and normal apps can't get it. Mobile-Expensify#14046 needs the same change.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can go further by putting this receiver in a separate AndroidManifest, along with PgoProfileReceiver and PgoProfileWriter in a
pgofolder.