Skip to content
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

RJS-2101: "Build optimizations" for iOS and Android #6650

Open
wants to merge 98 commits into
base: main
Choose a base branch
from

Conversation

kraenhansen
Copy link
Member

@kraenhansen kraenhansen commented May 3, 2024

What, How & Why?

Important

With this change, our SDK is no longer pinned to a specific ABI of JSI and we expect the next release of our SDK (after merging this PR) to be backwards and forwards compatible with many past versions of React Native.

This is my friendly takeover of #6396, by reverting the compilation of Realm Core from source and instead distributing prebuilds via our NPM package:

  • For React Native Apple (npm run prebuild-apple --workspace realm):
    • Prebuild format is an XCFramework of Realm Core
    • Deferred compilation on the developers machine is our binding (between Realm Core and React Native Core & JSI) driven by CocoaPods & XCode.
  • For React Native Android (npm run prebuild-android --workspace realm):
    • Prebuilds format is a CPack install directory (containing CMake config files)
    • Deferred compilation on the developers machine is our binding (between Realm Core and React Native Core & JSI) driven by Gradle calling into CMake.
  • For Node.js we're able to rely on the ABI stability of N-API and we produce prebuilds containing both Realm Core and our binding (between Realm Core and N-API) (npm run prebuild-node --workspace realm).

This also moves all binding related code (prebuilt or not), including that previously in packages/realm/react-native into packages/realm/binding in platform specific directories.

We won't support compiling our prebuilds from from the NPM archive as users still need to git clone our mono-repository to produce a build from source.

It's worth mentioning that this PR introduce an internal CLI incapsulating the complexity of invoking the CMake scripts for Android and Apple platforms. I imagine we could extend this to wrap cmake-js to provide a single cohesive experience for building our prebuilds in the future: This is a replacement for the packages/realm/scripts for Android and iOS that we currently use.

This is the resulting size of our tarball:

npm notice === Tarball Details === 
npm notice name:          realm                                   
npm notice version:       12.9.0                                  
npm notice filename:      realm-12.9.0.tgz                        
npm notice package size:  212.5 MB                                
npm notice unpacked size: 1.1 GB                                  
npm notice shasum:        f2bc7cffc60ee97a6c00161fefe3bac05b7becae
npm notice integrity:     sha512-V8wGrC2KNZe3O[...]Q5r/SEfraxqdg==
npm notice total files:   2895

Here's a bit more context on this change and why we choose the approach we did:

As we started implementing out "React Native SDK Build Optimizations" project we realized a few things:

  • Although we are generating 11K lines of C++ from the binding generator, it’s very fast to compile - most likely because it’s a single translation unit.
  • This generated binding layer is the only code dependent on the React Native headers / ABI.
  • We need Cmake to generate the Xcode project to compile Realm Core, which might not be installed on an app developers machine (in the correct version) but we can compile the binding layer without it.
  • If we choose to leave out the binding layer from the prebuilt binary (and we can because of ☝️) we don’t need to produce separate prebuilds for each React Native version.
  • Because of ☝️ we actually don’t have to delay the download of the prebuilt until “pod install” nor build-time. We could actually include it as an xcframework into the archive that we upload to NPM, like we do now. The main drawback of this approach is the download time for users as they “npm install” our package, but that won’t be a regression in DX to what we currently have and it doesn’t seem to be a deal-breaker to anyone as-is.
  • For Android, the situation is slightly more complicated, since Realm Core is dependent on ABI of the C++ Standard Library brought in by the NDK. Since the NDK version is configurable by the developer, this becomes a bit more complicated. Luckily the React Native template app (and Expo) declares a default for this NDK version, which we could rely on for the prebuild we upload to NPM, as long as we take great care to error if developers try using this prebuild with an unexpected NDK version and provide an easy way to build Realm Core from source, for those developers choosing a different NDK than the default.

In the context of the above, if we choose to include prebuild binaries for iOS and Android of Realm Core in the NPM package, which will be ABI independent on the React Native version (except for the caveat of NDK version, which has a default that I'd expect 95+% to leave unchanged) ... do we actually need to support compiling from source when installing the realm package via NPM as the original project scope suggests? I'd expect the need for that to be very rare (as it is right now) and it does come with drawbacks:

  • We'll have to avoid hoisting shared dev-dependencies to the root package of our mono-repo
  • Depending on a decision of forcing developers that wants to use this to have to npm install in their node_modules/realm we might need to include these dev-dependencies as actual dependencies of realm. Which would add to the install time of our package.
  • We'll most likely have to run tests to ensure all relevant dependencies are actually available to build from source when the realm package is installed outside of the mono-repo.
  • We'll have to include all of the Realm Core source-code into the repository, not a huge deal - but it all adds up.

The alternative is to ask developers that want to consume Realm JS and Realm Core from source to perform a checkout of the repository, run the relevant build script and npm pack the SDK package themselves.


Notes for the reviewer:

  • We deleted the packages/realm/binding/CMakeLists.txt and instead either refer to the CMakeLists.txt in bindgen/vendor/realm-core directly or CMakeLists.txt files for the individual platforms, instead of having an shared CMake project used by all bindings. This, because it felt the requirements for each platform was too different across platforms to play for the complexity of having a shared project.

☑️ ToDos

  • 📝 Changelog entry
  • Include before / after size of the NPM package.
  • 📝 Compatibility label is updated or copied from previous entry
  • 📝 Update COMPATIBILITY.md
  • 🚦 Tests
  • 📦 Updated internal package version in consuming package.jsons (if updating internal packages)
  • 📱 Check the React Native/other sample apps work if necessary
  • 💥 Breaking label has been applied or is not necessary

@kraenhansen kraenhansen self-assigned this May 3, 2024
@cla-bot cla-bot bot added the cla: yes label May 3, 2024
@kraenhansen kraenhansen force-pushed the kh/build-optimizations-squashed branch 2 times, most recently from e0482ca to 0b3a7e7 Compare May 3, 2024 12:22
@kraenhansen kraenhansen force-pushed the kh/build-optimizations-squashed branch 6 times, most recently from 943dabc to 29868fe Compare May 23, 2024 14:50
@kraenhansen kraenhansen changed the title "Build optimizations" for iOS and Android RJS-2101: "Build optimizations" for iOS and Android May 24, 2024
Comment on lines -438 to -445
- uses: actions/setup-java@v3
if: ${{ matrix.variant.os == 'android' }}
with:
distribution: 'zulu' # See 'Supported distributions' for available options
java-version: '17'

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines -123 to -138
- name: Setup Java
if: ${{ matrix.variant.os == 'android' }}
uses: actions/setup-java@v3
with:
distribution: 'zulu' # See 'Supported distributions' for available options
java-version: '11'

- name: Setup Android SDK
if: ${{ matrix.variant.os == 'android' }}
uses: android-actions/setup-android@v2

- name: Install NDK
if: ${{ matrix.variant.os == 'android' }}
run: sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were moved into a separate Android specific prebuild-android job.

Comment on lines 102 to +114
"pod-install:ci": {
"command": "pod-install",
"clean": "if-file-deleted",
"files": [
"ios/Podfile",
"../../../packages/realm/RealmJS.podspec"
],
"output": [
"ios/Pods",
"ios/Podfile.lock",
"ios/RealmTests.xcworkspace"
],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for dependencies on other scripts when testing on CI, since that's already handled by other jobs uploading artifacts.

@@ -1,111 +0,0 @@

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting this entirely, since almost all the information was outdated and would have to be re-tested anyway.


s.frameworks = uses_frameworks ? ['React'] : []

s.library = 'c++', 'z', 'compression'

s.pod_target_xcconfig = {
# Setting up clang
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++20',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using C++20 features in the binding.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we merge or close #5053?

Comment on lines +918 to +919
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this to silence warnings that might be confusing to our users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But will we accidentally overlook something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say very unlikely, since it's pop'ed just after the code that relies on it.

[bridge jsCallInvoker]->invokeAsync(
[&]() { waitingForUiFlush = false; });
[&]() { self->waitingForUiFlush = false; });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this to silence warnings that might be confusing to our users.

const { submitAnalytics } = require("../dist/scripts/submit-analytics");
submitAnalytics().catch(console.error);
} catch (err) {
console.error(err instanceof Error ? err.message : err);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only prints if loading the analytics file fails, since submitAnalytics returns a rejected promise if failing. This is mostly to avoid any uncaught exceptions resulting in an unclean exit code.

@@ -0,0 +1,172 @@
////////////////////////////////////////////////////////////////////////////
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the build script for the Android prebuild.

@@ -0,0 +1,181 @@
////////////////////////////////////////////////////////////////////////////
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the build script for the Apple / iOS prebuild.

@@ -42,7 +42,6 @@
// the other information on.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I moved more scripts into TypeScript I took the time to "translate" the submit-analytics script as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript first team 😄

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const realmPackagePath = path.resolve(__dirname, "..");
const realmPackagePath = path.resolve(__dirname, "../..");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It moved up a level because of the src directory.

Comment on lines 118 to 130
function isAnalyticsDisabled() {
let isDisabled = false;

// NODE_ENV is commonly used by JavaScript framework
if ("NODE_ENV" in process.env) {
isDisabled |= process.env["NODE_ENV"] === "production" || process.env["NODE_ENV"] === "test";
if ("REALM_DISABLE_ANALYTICS" in process.env || "CI" in process.env) {
return true;
} else if (
// NODE_ENV is commonly used by JavaScript framework
process.env["NODE_ENV"] === "production" ||
process.env["NODE_ENV"] === "test"
) {
return true;
}

// If the user has specifically opted-out or if we're running in a CI environment
isDisabled |= "REALM_DISABLE_ANALYTICS" in process.env || "CI" in process.env;

return isDisabled;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored this to use returns instead of the more exotic |= operator.

@@ -175,15 +170,15 @@ function getInstallationMethod() {
if (userAgent) {
return userAgent.split(" ")[0].split("/");
} else {
return "unknown";
return ["unknown", "?.?.?"];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually a bug I discovered during the refactor.

Comment on lines 309 to +313
const payload = {
webHook: {
event: "install",
properties: data,
},
event: "install",
properties: data,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to wrap this in a webHook object, since it was always used as payload.webHook.

Comment on lines -353 to -355
if (process.argv[1] === fileURLToPath(import.meta.url)) {
submitAnalytics().catch(console.error);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This remains in the calling <root>/script/submit-analytics.js.

kraenhansen and others added 27 commits May 31, 2024 15:53
Co-authored-by: Kenneth Geisshirt <kenneth.geisshirt@mongodb.com>
@kraenhansen kraenhansen force-pushed the kh/build-optimizations-squashed branch from fe44d2d to f84f332 Compare May 31, 2024 13:54
This was referenced May 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify by unifying build commands
2 participants