Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

RFC: Apennine Architecture (multi-bundle support) #127

Closed
zamotany opened this issue May 20, 2019 · 41 comments
Closed

RFC: Apennine Architecture (multi-bundle support) #127

zamotany opened this issue May 20, 2019 · 41 comments
Labels
🗣 Discussion This label identifies an ongoing discussion on a subject

Comments

@zamotany
Copy link

zamotany commented May 20, 2019

RFC: Apennine Architecture

The Apennine architecture builds up the mountain of JS dependencies that are common to any RN app running on the platform, providing a ready and waiting foundation from which any app can quickly run.

Apennine aims to build this mountain as high as possible, by not only pre-evaluating common JS dependencies, but also initializing the framework through the point of starting an empty application that can host any app's components.

Disclaimer: for simplicity we are referring only to Android implementation here.

Architecture breakdown

When using the Apennine architecture the app would consist of at least 3 bundles:

  • Host bundle - contains runtime logic for communication and it's responsible for updating the React View tree.
  • DLL bundles - common dependencies (like react, react-native) or shared logic packed together.
  • App bundles - application logic

bundle_relationship

Both DLL bundles and app bundles can be a regular JS bundle (BasicBundle) or a Indexed/File RAM bundle.

This multi-bundle architecture would be available not only in release mode like RAM bundles, but also in development mode and served from packager server, to allow for easy JS debugging with Remote debugger for all of the bundles including Host bundle and DLL bundles.

BundleRegistry

The single point for holding all bundles and managing them would be in BundleRegistry (which itself would be stored in Instance). Each bundle would be given it's own BundleExecutionEnvironment.

BundleExecutionEnvironment would contain NativeToJsBridge instance (with JsToNativeBridge) as well as MessageQueueThread and a JSIExecutor, which would hold a JSContext for given bundle.

All bundles would be stored in a std::vector<Bundle> and have a Bundle* initialBundle in BundleExecutionEnvironment, so that BundleExecutionEnvironment knows, which bundle to load first. From there, the loading of additional bundles will be triggered from JavaScript. BundleRegistry would also expose an API to be used on the native side.

bundle_registry

Bundles overview

Formats

A JS source code can be stored in one of the following formats:

  • BasicBundle - regular bundle format, where all JS is loaded evaluated at once (the results for running metro/haul bundle command).
  • IndexedRAMBundle - Single-file RAM bundle, where all modules are stored in single file, but loaded and evaluated lazily with constant lookup time (the results for running metro/haul ram-bundle --indexed-ram-bundle command).
  • FileRAMBundle - Multi-file RAM bundle, where all modules are split into separate files metro/haul ram-bundle --platform=android.
  • DeltaBundle - Delta bundle used by metro.

Each bundle format would derive from abstract Bundle class with the following public interface:

class Bundle {
  public:
    Bundle() = default;
    Bundle(const Bundle&) = delete;
    Bundle& operator=(const Bundle&) = delete;
    virtual ~Bundle();

    virtual void makeJavaScriptApi(std::weak_ptr<BundleRegistry> bundleRegistry,
                                   std::weak_ptr<JSIRuntime> runtime) const = 0;
    virtual void load(std::weak_ptr<JSIRuntime> runtime) const = 0;
    virtual std::string getSourcePath() const = 0;
    virtual std::string getSourceURL() const = 0;
}
  • makeJavaScriptApi would be responsible for preparing the JS environment, providing globals (like nativeRequire) etc.
  • load would evaluate bundle's source code in given runtime.

Host bundle

Format: BasicBundle.

Content: Runtime logic and the only point, where AppRegistry.registerComponent should be present. Once the appSwitch event is received it should take new App's root component from global BundleRegistry and render it.

Notes:

  • We can extend functionality to support rendering multiple apps in single screen or View Controller.
  • Host bundle must be always present when running in multi-bundle mode.
  • Host bundle must call BundleRegistry.initializeHost(); before doing anything.

DLL bundles

Format: BasicBundle/IndexedRAMBundle/FileRAMBundle.

Content: Common dependencies and shared logic.

Notes: React Native app can use 0, 1 or more DLL bundles. It's up to the developer to deicide which amount of DLL is the best for their use case and properly split them.

App bundles

Format: BasicBundle/IndexedRAMBundle/FileRAMBundle.

Content: Application logic, components with root component exported as default export:

// App0 bundle
/* ... */

export default App0RootComponent;

Notes: React Native app can use 1 or more app bundles. It's up to the developer to deicide how to split those.

Backwards compatibility

Be default BundleRegistry would be running in single-bundle mode and support single-bundle application regardless of bundle format. Calling BundleRegistry.initializeHost() from JavaScript effectively switches into multi-bundle mode. This means that the older apps or apps that don't use multi-bundle functionality should work the same.

Single vs multi-JSContext mode

Each App bundle and Host bundle would have it's own JSContext assigned. All of those JSContexts would be in the same group to allow for cross-JSContext communication in a synchronous manner. This is to ensure that when app bundle is loaded and it's root component is added to BundleRegistry, the Host bundle running in Host JSContext can obtain the JSValueRef to the root component and render it.

The trigger to load new bundle (DLL or app) will can be done from both JS and Native code and the developer can decide if they want to load bundle in current execution environment (same JSContext) or a different one, but calling appropriate function:

BundleRegistry.loadInCurrentEnv('bundleIdentifier', { isAppBundle: true });
BundleRegistry.loadInNewEnv('bundleIdentifier', { isAppBundle: true });

If isAppBundle is true, it will notify the Host bundle in Host JSContext (which can be the same as JSContext to load bundle to if loadInCurrentEnv is used) to use new App root component and render it.

WIP: figure out how to provide cross-JSContext communication if JSContexts are running on separate threads.

Bundle creation on native side

On native side the decision which bundle format to choose would be made in implementation of the following functions:

  • CatalystInstanceImpl::jniLoadScriptFromFile
  • CatalystInstanceImpl::jniLoadScriptFromDeltaBundle
  • CatalystInstanceImpl::jniLoadScriptFromFile
  • CatalystInstanceImpl::jniLoadScriptFromAssets

Instead of creating RAMBundleRegistry, JSDeltaBundleClientRAMBundle or passing JSBigString would create an instance of BasicBundle/IndexedRAMBundle/FileRAMBundle/DeltaBundle and move it as std::unique_ptr<Bundle> to Instance::loadBundle, which would evaluate the given bundle BundleRegistry.

Note: Source URL would be stored in instance of Bundle for later retrieval.

Bundle creation

To create Host, DLL and App bundles, we would use Webpack's DllPlugin and DllReferencePlugin together some custom logic in Haul.

Discussion points

At this point we need to gather as many feedback regarding the native implementation and architecture as possible. Those changes will affect the core functionality of React Native so it is crucial to make sure nothing is broken. There might be small nuances, that I'm not aware in ReactCommon or CxxReact - if you know about any of those, please share it with us.

Other open questions are:

  1. Should we use single callback or multiple ones in Instance::initializeBridge?
  2. What about navigation?
  3. Cross-JSContext communication and data sharing in user space?
  4. How to differentiate View IDs on native side?

Co-Authored with @dratwas and @joeblynch

cc: @matthargett @nparashuram @fkgozali @TheSavior @cpojer @kelset @thymikee @dratwas @grabbou @satya164

@zamotany zamotany changed the title RFC: Apennine Architecture RFC: Apennine Architecture (multi-bundle support) May 20, 2019
@kelset kelset added the 🗣 Discussion This label identifies an ongoing discussion on a subject label May 20, 2019
@axemclion
Copy link

@zamotany, Thanks for writing this very detailed proposal, this is awesome. I think I got most of what you are proposing, so here are some questions I had. Also, please correct me if some of the assumptions in my questions are incorrect.

As I understand, you are proposing this architecture for RN production apps, right ? Are you proposing this architecture for every single RN app, or more for super apps ? While I understand that this will greatly help super apps, how would it help standalone react native apps. Those apps will have to load the host, and the DLL parts, in addition to the actual bundle. With inline requires and RAM bundles, we could already evaluate and run code lazily - how else will this work ?

@zamotany
Copy link
Author

@axemclion

you are proposing this architecture for RN production apps, right

Yes. All RN apps would be running on this architecture, however decision whether to leverage multi-bundle feature is up to the developer and it's opt-in - it will be up to developer, if they want to use Host/DLL/apps bundles or a single regular/RAM bundle.

standalone react native apps. Those apps will have to load the host, and the DLL parts, in addition to the actual bundle

Not necessarily, as I mentioned in Implementation breakdown, the trigger for loading additional bundles (DLL/app) is coming from JS, so if the Host bundle is actually just a regular bundle (non-RAM/RAM), with application code, then nothing else will ever be loaded, since there's no code for that and the Host bundle will effectively become a normal application bundle (non-RAM/RAM) - the bundle we're all using currently.

With inline requires and RAM bundles, we could already evaluate and run code lazily

Inline requires and RAM bundles does help with reducing TTI/TTR, however putting everything in single RAM bundle doesn't scale well.

@axemclion
Copy link

I think my question is - can this not be done today with the unbundle (RAM bundle command), where the files are split ? the "import" command sort of already does this, right ? Sorry, I am having trouble understanding the difference.

@karanjthakkar
Copy link

The three biggest differences that I think are:

  • With RAM bundles you cant load all the code for a screen in one go. It sequentially fetches all dependencies for a bundle iiuc.
  • With this, it will be possible to give the user more control on loading an upcoming screen's code ahead of time based on some heuristics. Especially useful in brownfield apps. For ex: An app that has a native home screen can just pre initialise the bridge with common code and then lazily inject code for an entire screen just in time or slightly before there is a possibility that a user might click on it.
  • Lastly, indexed RAM bundles for Android, which is where most apps usually need the speed improvement, have been broken for a very long time, 0.57-0.59 for sure, maybe even before that.

@zamotany
Copy link
Author

@karanjthakkar there's already PR to fix Indexed RAM bundles on Androrid: facebook/react-native#24967

@karanjthakkar
Copy link

@zamotany This is super well thought! Good job! I've been wanting for something like this since I did this lazy bundling experiment last year. If you want to chat about implementing this from the iOS side, I'd be happy to help!

One of the questions that I have after reading this is, do you expect Haul to be the default bundler/packager if this were to be baked into RN? I don't know if metro supports this kind of code splitting atm.

@karanjthakkar there's already PR to fix Indexed RAM bundles on Android: facebook/react-native#24967

Wow! Thank you! I'm going to give this a try internally and see how it works out!

@zamotany
Copy link
Author

zamotany commented May 20, 2019

@karanjthakkar Not really, for an average RN developer dealing with Haul and using this architecture might not be worth it. The main target audience here are big companies, where ability to scale easily is crucial. Webpack ecosystem has it's own shortcomings, so I don't see Haul as a replacement of Metro but rather an an alternative - for developers/companies whom will actually benefit from Haul + multi-bundles.

@karanjthakkar
Copy link

@zamotany Got it. So folks who want to use multi-bundles will essentially need to use Haul is what I wanted to confirm.

Another question, how does the native side decide if it needs to run in multi bundle mode? Does the host bundle have some identifier to indicate that?

@zamotany
Copy link
Author

@karanjthakkar Unless the Host bundle requests to load additional bundles (DLL/app) from JS, it's basically operating in single-bundle mode, so no - there's no need for any additional identifier. In other words: by default it's running in single-bundle mode, when the trigger to load new bundle is received from JS, then it's running in multi-bundle mode.

@joeblynch
Copy link

Thanks @axemclion for your questions! Full disclosure, this architecture initially came out of some internal work I did, and @matthargett and I have been working with Callstack to build an implementation of this architecture.

For standalone RN apps, the intention is that they would still be able to be deployed as a single RAM bundle, without any changes to how they’re deployed.

I think your mention of super apps is a great example of where this could provide benefit over a single RAM bundle. For super apps, the “mini apps” would be able to be built against a common DLL bundle that’s provided by the super app, and deployed independently.

The main benefit being that mini apps would be able to be run in an independent JS context from the super app. The independent JS context can be pre-warmed with the host app and DLL bundles, before it’s known what mini app will be loaded next. When a mini app is loaded, only its app-specific code needs to be evaluated, removing the initialization time of the common dependencies from the TTI. Ideally this can bring the TTI time down to be on par with the duration of the animation used to transition to the mini app.

Another example might be something like the Facebook app. I don’t know its internals at all, but let’s say hypothetically that Marketplace and Profile are separate bundles. Code that is common to both, such as react, react-native, data access libraries like relay, common telemetry libraries, etc could be moved into the DLL bundle and preloaded into the host app in the background. Once the user selected the Marketplace or Profile tab, just the code specific to that tab could be loaded, providing a middle ground between a cold boot of those tabs and having them completely preloaded.

@acoates-ms
Copy link
Collaborator

We use something very similar in Office.

Boot perf is one benefit, although it would possibly be able to be optimized by a perfect bundle delay load solution instead. But another benefit of the separate instance solution is the ability to shut down the "mini apps". Using the Facebook app example, assuming Marketplace and Profile are both RN solutions. As you navigate around the Facebook app, loading more and more RN solutions, more and more of the bundle would get parsed and loaded into the instance. So once you've ever navigated to both the Marketplace and Profile, your instance would be forever the size of both those apps together. As the number of mini apps goes up, this becomes a massive memory hog.

What we want is the ability after the user has navigated away from the MarketPlace, for the app to be able to shutdown the Marketplace app, and return to a memory usage based on just what the user is currently using. -- Otherwise RN features become essentially massive memory leaks.

Having at least part of the bundle be shared between the instances also allows for optimizations around memory mapped files that allows the disk read to be consolidated between those instances, for additional performance gains.

@oximer
Copy link

oximer commented May 21, 2019

In our company, we also have a scenario very similar to what you are describing and our current app is already working in a multi-bundle scenario. It's really good to see our teams working on this scenario! We should work together to find a better solution to it.

As a Bank, our App has more than 100 difference services on it: credit cards, insurance, loans, Financial Tracker, Payments and many others. Many of those services are developed and owned by different teams. Each one of these team have a different backlog and business priorities. These services/team were mini apps inside my super app.

In the last months, my team came with a simple solution to overcome schedule conflict's and bugs from mini app 1 impact on mini app2. We gave each mini app a bundle and let them develop and deploy it on their own time. A wrote a medium post about it.

https://medium.com/@joseurbanoduartejunior/pushing-react-native-to-a-new-architecture-level-4750777eb07a

On my post, I describe same problems that the face. In addition to those described there, loading time and the memory consumption as described by @acoates-ms were another problems. However, the app have being used by more 15 M users without big problems.

After a couple months in production, we were planning to design a system similar to what @zamotany
suggest. A DLL Bundle would be awesome, reducing bundle size and reducing loading time.

I want to endorse this suggestion and offer our help. I'm really down a Skype meeting.

@zamotany
Copy link
Author

@joeblynch @acoates-ms @oximer
The proposed architecture in it's current form would use a single JSContext for all the apps. Even with this limitation we can add memory management functionality to release unused modules - for example when navigating from app0 to app1 if some flag to release memory is true the Host bundle can iterate over modules left from app0 and unused by app1 and delete them from the module cache.

@oximer
Copy link

oximer commented May 21, 2019

@zamotany Why using a single JSContext would be better?

If we use two JSContext, we could avoid JS bugs from App 0 to affect App1. In addition, we avoid some issues with privacy and sensitive data between bundles.

It would be pretty similar to different tabs inside a browser.

@zamotany
Copy link
Author

@oximer Yeah, that's true. After talking with @joeblynch, we decided to revisit the proposed architecture to make it use multiple JSContexts for each app. I'll update the issue soon.

@gvarandas
Copy link

As an (now) external member of @oximer team, I’m stepping up to participate in the discussion as well.
This is very exciting and hopefully will engage more large scale companies into the React Native community.

@karanjthakkar
Copy link

I might not be able to see the bigger picture yet but some disadvantages with multiple contexts I can think of are:

  • Massive memory consumption, especially in low end androids (for whom we actually want to make things faster with this), because each app would need to load all the common libraries and allocate memory for them in each context
  • They would be able unable to share any common data which seems really unfortunate because you end up with the problem of overfetching unless you have two absolutely isolated mini apps with no shared data within your app

If we want to use multiple JSContext's, then is it currently not possible to do that by calling initWithBundleURL(for iOS) with a new bundle location for every mini app?

@joeblynch
Copy link

joeblynch commented May 21, 2019

@karanjthakkar one option I had very briefly explored internally was the idea of being able to have an "internal deeplink" within the host app, which would allow additional app bundles to be loaded into an existing host app context. Like you mentioned, this was primarily to allow for data to be shared between "mini apps", to reduce memory overhead, and to allow the pre-warmed "host+dll" context to be left running in the background for the next "external deeplink" that needed an isolated instance.

For the memory reasons that @acoates-ms mentioned, and the isolation requirements that @oximer mentioned, we also have cases where it's beneficial to have a separate instance for "mini apps" at times. Perhaps a solution that supports both use cases would be ideal?

@matthargett
Copy link

@karanjthakkar In terms of over-fetching, having a shared, transformed cache (e.g. Apollo GraphQL) on-disk shared between apps is one way to address your concern. Re-fetching is certainly a thing to optimize away from, but so is re-parsing the JSON and HTTP cache headers you did fetch. Keeping each app's in-memory cache tuned to be a low as reasonable for the real-world user flows, and relying on the shared disk cache, gets you the safety and the performance (CPU, memory, and heat/battery life).

@ide
Copy link
Member

ide commented May 21, 2019

Perhaps a solution that supports both use cases would be ideal?

This seems right to me. In my experience, having the option to share data between sub-applications is very useful in an organization whose lifeblood is product iteration. Put another way, using a single JSContext preserves flexibility to change the lines of division in both the product and the org chart.

At the same time, for some products the lines of division are intrinsic and likely to stay the same. The super apps that Ram mentioned are one of the better examples of legitimate lines of division between sub-applications. MS Office or something like Xbox are also good examples (ex: isolating Word from Excel or the Xbox store from Xbox settings). So I think multi-bundle support is desirable for both the single-JSContext and multi-JSContext scenarios -- or if only the single JSContext scenario is supported, for another JSContext to be able to load code from the same source as other apps.


This said, here are a few deeper principles I think a good system would exhibit:

  • Split DLL bundles: The DLL bundle will become very large over time and splitting it will provide many of the same benefits as splitting the current monolithic bundle. One example is if you have three apps and two of them share a dependency that is placed in the DLL bundle(s). With multiple DLL bundles, the app that doesn't need that dependency might not load the DLL bundle that contains the unused dependency.
  • Browser compatibility: Emitting bundles that could run in a browser -- perhaps with some small modifications -- has two significant benefits: (a) RN has a smaller surface area that can be part of a stable 1.0 release and (b) it is easier for other bundlers like Rollup or Parcel to generate bundles that work with RN. The less RN knows about Host/DLL/App bundles, the more powerful. This is primarily about making RN more standards-compliant and therefore closer to working with a wider set of existing web tools.
  • Flexible splitting policies: Being able to split in several different ways -- based on usage patterns (covariance), code thrash, or priority (render-blocking code before analytics code, for example) is far more powerful than using lazy import() as split points or a hand-tuned vendor bundle. Lazy import()s or a vendor bundle list could still be provided as inputs to the bundler but aren't the only signal. This is more of a problem for the bundler to solve, but the runtime can help by not prescribing the split points.

I don't see these as requirements but think they are guiding principles towards a small, stable 1.0.

@zamotany
Copy link
Author

@ide Those guiding principles you mentioned are more targeted towards bundlers, not this architecture. What we are proposing here is the architecture that would support multiple bundles - how would you use them and how the bundles would be generated is up to the bundlers. For instance, if someone wants to have multiple DLL bundles, nothing is preventing that in Apennine Architecture from happening, only the packager needs to create those multiple DLL bundles and the Host bundle needs to load them. Apennine Architecture could support as many bundles as you want + JS API to load and handle them.

The less RN knows about Host/DLL/App bundles

What the RN would need to know is that there can be multiple bundles and support an JS API to manage them.

@oximer
Copy link

oximer commented May 22, 2019

In our project for sharing data between bundle, we boost the AsyncStorage module. When you save something on the smartphone HD, you can save it on a common area for your app or on your mini app. area. To avoid concurrency, we add a prefix (bundle_name) to all keys saved to AsyncStorage.

There are also other scenario, as example, where a (mini app) bundle wants to start another (mini app) bundle. We also solve it, creating some extra React Native Native Modules. Navigation between mini apps introduce some complexity, but we figure out a way to solve it.

The first step on this thread should be allowing multi bundle using DLL.
After solve it, we can discuss further aspects such as (AsyncStorage, Navigation, Communication between Bundle in different JSContext, etc).

@statm
Copy link

statm commented May 22, 2019

We did some experiments in an attempt to achieve similar goal.

With webpack's DLL plugin, all the inter-ops between bundles will rely on module IDs (rel paths in dev, numbers in ship). This poses several limitations:

  1. If a bundle is modified and rebuilt, all other bundles that depend on it will also have to be rebuilt, otherwise there might be module ID mismatches at runtime.
  2. For dev flavor bundles, because their module IDs are relative paths, all of them need to be built under the same project structure, otherwise there might be module duplication across bundles.

@joeblynch
Copy link

The first step on this thread should be allowing multi bundle using DLL.


@zamotany @oximer @karanjthakkar I’ve been thinking more about this, and perhaps at least for the type of work described in the implementation breakdown, it makes sense to focus on the loading of multiple bundles into a single JSContext.

For the case of multiple contexts within a super app, maybe that’s a related but somewhat tangental problem. To implement that, we use a component that’s basically akin to an iframe. This gives the super app full control of the loading and positioning of mini apps. Of course, this also requires that there’s a mechanism to map the URL given to the iframe component to the file path of the bundle being loaded.

Within the super app, one of these iframe components can created in the background. If and when the super app choses, it can load the host and DLL bundles into the JSContext running in the iframe component, to pre-warm whatever mini app the user choses to launch next.

Once the super app then loads an app bundle into the iframe, it can move that component into view to display the mini app. At that point, the super app could optionally pre-warm another iframe/JSContext in the background to prepare for the next mini app that gets loaded, or it can reuse the existing iframe/JSContext instead.

@joeblynch
Copy link

@statm very good point! If the module IDs were something like hash(package name of the DLL + relative path to the module), do you foresee those issues still being a problem?

@CaptainNic
Copy link

@joeblynch

I’ve been thinking more about this, and perhaps at least for the type of work described in the implementation breakdown, it makes sense to focus on the loading of multiple bundles into a single JSContext.

This sounds very close to what ram-bundle/unbundle already provides.

@joeblynch
Copy link

This sounds very close to what ram-bundle/unbundle already provides.

@CaptainNic it's similar, the distinction is that the host and DLL bundles are able to be deployed separately and shared by multiple app bundles. Since they're shared, they can be preloaded before it's known what app bundle will be loaded next.

@statm
Copy link

statm commented May 22, 2019

@joeblynch It will help partially, but won't solve all these issues. Imagine the host bundle is built in one mono-repo, and App A bundle is built in another repo. Both contain module runtime.js. Its ID in two bundles might be different:

  • Module runtime.js in host bundle: ../../../common/temp/node_modules/regenerator-runtime/0.11.1/node_modules/regenerator-runtime/runtime.js
  • Module runtime.js in App A's bundle: ./node_modules/regenerator-runtime/runtime.js

In this case, as long as the hash contains relative paths, runtime.js will not be pointed to host bundle.

@ide
Copy link
Member

ide commented May 22, 2019

@zamotany It would be nice for the design to move more of the logic and decision-making to the bundlers. As you wrote -- "Those guiding principles you mentioned are more targeted towards bundlers, not this architecture" -- I would love for the design to shift the principles to the bundlers.

On React Native's side, this could happen by keeping the API small (both number of functions and what those functions can do), down to maybe just a couple variants of loadScriptFromX -- and for the notion of Host/DLL/App bundles to be in the emitted JS rather than in React Native.

Currently in the proposal, React Native is aware of bundles and startup code. I'm suggesting to follow a browser-like model (principle 2) and expose a few script execution functions that the JS can call. Then this starts to open up pathways towards user-space HMR (ex: webpack-dev-server), different forms of bundle splitting and code reuse, and other features that JS can implement without needing to change React Native.

@zamotany
Copy link
Author

@axemclion @karanjthakkar @joeblynch @acoates-ms @oximer @ide @matthargett

I have updated the issue with new details to account for multiple JSContexts and added new points for discussion. Please check it out and if you have any answers or feedback to those, don't hesitate to share it with us.

@adrianha
Copy link

I have researched the mini app approach in my company, i can share some difficulties that i'm facing using this concept:

  • Error JS Reporting, currently many of RN community using Sentry / Bugsnag to be able monitor the error in production side, the mini app approach, since it's load multiple bundle into 1 JavascriptCore environment make cannot be trace properly. Worth to mention, Sentry & Bugsnag currently only support Indexed RAM Bundle format, and not File RAM Bundle.
  • Over the air update, the DLL Bundles, "hard" to be updated over the air, because like CodePush instance support live in single JSContext, but by that means, it's difficult to ensure who and when DLL should be updated. Which mini app is responsible to update DLL bundle.
  • Also many native packages out there like CodePush is developed using singleton concept so its not possible and need to be adjusted to have multiple instance on 1 RN application.

@zamotany
Copy link
Author

@adrianha

Error JS Reporting, currently many of RN community using Sentry / Bugsnag to be able monitor the error in production side, the mini app approach, since it's load multiple bundle into 1 JavascriptCore environment make cannot be trace properly

The Apennine Architecture allows you to decide if you want to run all bundles on a single JSContext or multiples ones - if you spin up new BundleExecutionEnvironment and load new bundle into it, the problem with stack trace is gone. As for running multiple bundles in a single JSConext - it is possible to have correct stack track, especially in RAM bundles, since the filenames in stack are just a module ID, so by processing and concating Indexed Source Maps you can get a valid source map - module mapping, but you have to know upfront if the bundle will be run on the same JSConext.

@cdsanchez
Copy link

This is really great! Few questions:

  1. Is there any provision in this proposal for allowing applications to hook into or intercept the BundleRegistry.load* calls? This would be useful for loading from different sources.
  2. Will the BundleRegistry support bytecode bundles for integration with hermes?
  3. Has anyone expressed interest in adding this functionality to metro?

@cpojer
Copy link
Member

cpojer commented Jul 24, 2019

It appears that the PR with the proposed implementation was sent before we reached consensus. I would like to propose a way forward for this RFC and a possible implementation.

Current State

Consensus is important because:

  • The RN team at Facebook is planning large changes to this area over the next 6–12 month period and need to make sure that our plans don't conflict with this proposal.
  • All core contributors and Facebook will be responsible for maintaining these changes after they are merged.

The size of the PR is an issue because:

  • We use React Native from master at Facebook. Any PR that we merge will instantly (and before even appearing on GitHub!) be part of all the apps we develop at Facebook. @TheSavior wrote a longer explanation that I recommend reading: https://gist.github.com/TheSavior/608dfb8d5f80a0eef0f0b1681db5ec95. This constraint means that any single change to React Native needs to be well tested and cannot regress performance, app size or memory.
  • While we do have extensive testing for many things, not everything is caught before shipping apps to our users. The process we use at Facebook to unblock ourselves from issues caused by a problematic commit is to revert the change and then ask the author to commit it again later with the issues resolved. This takes longer when we have to revert Pull Requests and ask the author to redo their work. Please also keep in mind that this can often take Facebook employees multiple days of work that they would otherwise spend on improving React Native.

This process certainly has trade-offs and you may have opinions about how we could do things differently. I am trying to explain why it is currently hard for us to integrate large changes such as this and why they need to be broken down into fully-working isolated changes step-by-step.

Moving Forward

I propose the following steps to unblock this work that have the highest chance of getting included into RN:

  • Further discuss this proposal here and reach consensus.
  • Send multiple small isolated and well-tested Pull Requests to React Native that build towards the full implementation.

Proposed changes

As discussed here by @ide and privately with me and other FB employees we believe there could be a much less invasive first solution that will gives us most of the benefits of the proposed architecture and allowing others to build third-party modules around this area to make it even better. I propose:

  • Push most of this work into the bundling step. Metro already has the capability to do bundle splitting but it is currently somewhat manual. We may be able to share some code that we also shared with @airbnb's web team and we are happy to accept contributions to Metro for this.
  • Follow @ide's suggestion: "On React Native's side, this could happen by keeping the API small (both number of functions and what those functions can do), down to maybe just a couple variants of loadScriptFromX -- and for the notion of Host/DLL/App bundles to be in the emitted JS rather than in React Native." – this ensures that the surface area of this change stays small. I would even go one step further and propose to expose only a global function to load either JS or Hermes bytecode. Loading can be done via fetch and caching/storage can be done via a (third-party) native module.
  • At Facebook, we are now exclusively using Hermes bytecode bundles on Android. We do not use RAM bundles in production and haven't for more than a year. Hermes bytecode and inline requires provide similar or better performance improvements that RAM bundles provided. I would suggest focusing on that instead of RAM bundles.

(thanks @rickhanlonii for reviewing this)

@sahrens
Copy link

sahrens commented Jul 24, 2019

To reiterate the point about Hermes...from @acoates-ms:

What we want is the ability after the user has navigated away from the MarketPlace, for the app to be able to shutdown the Marketplace app, and return to a memory usage based on just what the user is currently using. -- Otherwise RN features become essentially massive memory leaks.

Having at least part of the bundle be shared between the instances also allows for optimizations around memory mapped files that allows the disk read to be consolidated between those instances, for additional performance gains.

I believe Hermes gives us all this on Android and it sounds like there was some unfortunate timing between this RFC and our efforts to release Hermes. Because the bytecode is a read-only mmapped file, it can actually return pages of bytecode back to the OS when they are no longer needed (I believe the GC can also return pages to the OS after collecting and compacting). @amnn might have more to add (or correct).

@joeblynch
Copy link

Thank you @cpojer and @sahrens for your feedback!

It sounds like Facebook has been thinking about how to do achieve TTI reduction similar to this, but that this approach we've taken is not the same as Facebook's ideas. Could you share some more specifics of what the solution you've been thinking about looks like? We want to make sure we can get it working for all the users of RN on platforms they currently deploy on, and hopefully also be able to give it to the community who will be using it without Hermes / on iOS for a while.

I agree fully that bytecode bundles are preferable over RAM bundles. I see the architecture proposed in this RFC as being a technique that's used in addition to bytecode, with both implemented to reduce TTI, but in different and complementary ways.

The primary target for this architecture is environments like super apps, in which multiple "mini apps" run in separate JS contexts, while sharing much of the same initialization process. Each mini app within the super app will need to stand up an RN app instance, and for example might need to initialize libraries for navigation, data access, telemetry, etc.

Since this work is common to every mini-app, there's no need to wait to do this work until it's known what mini app will launch next. Instead we preinitialize these common dependencies in a JS context, and when a mini app is loaded, its app bundle is evaluated in this pre-warmed context. The app's root component can then be mounted into the waiting RN app instance.

By using bytecode bundles instead of RAM bundles, we get the additional benefit that the preinitialization time and app bundle eval time are both reduced, by being able to skip the lexing and parsing work. In theory, if the JS VM in use supports heap snapshots, it may be even faster to load the pre-warmed context from a snapshot.

@matthargett and I discussed the proposed changes to move this forward with @zamotany today, and we are in agreement that we'd like to keep the required changes to the core of RN as lean as possible. He plans to comment further on the implementation of these changes.

@TheSavior
Copy link

TheSavior commented Jul 29, 2019

@joeblynch, could you help us understand more about your approach of super apps and if that approach is common in the industry?

For context, I think us at Facebook would consider the Facebook mobile app a "super app" 😉 . It has hundreds (thousands?) of distinct feature sets, and we try to think about things similarly. For example, when someone goes to Marketplace, and then to Dating, Dating should start up as quickly as possible with as little overhead as possible, and Marketplace should be able to be evicted, returning memory back to the operating system.

I'm sure your group is needing to make some different tradeoffs than we are for the Facebook app, so it might be helpful to have some more context on those tradeoffs in order to figure out how they impact approaches here.

@zamotany
Copy link
Author

@cpojer After some thinking with @joeblynch and @matthargett we have an idea how to move forward that would be easier for FB to approve:

First of all, we would split the work between refactoring and features, meaning the first PR (or set of PRs) would refactor the bundle types and logic around them. We'll aim at making the changes as small as possible in terms on PR size and bytecode size increase, so that it would be feasible to be merged.

After that, the feature works, as suggested, would improve upon existing loadScriptFromX, adding new one if necessary and changing it so that it can be loaded multiple times. The idea is to have a better native API on which the user/companies can built upon to meet their requirements. This also means that there would be no built-in JS API - this will have to be exposed using 3rd party native module (which would consume aforementioned native API). With that approach, we should have code in core as small as possible.

Please, let us know what you and the RN team at FB think about that. If there're any suggestions or comments, please write them here.

@rickhanlonii
Copy link
Contributor

@zamotany, @joeblynch, and @matthargett what do you think about @cpojer's proposal?

@zamotany
Copy link
Author

zamotany commented Jul 30, 2019

@rickhanlonii
I agree with having the core as small as possible and providing couple of variants of loadScriptFromX. Our idea is built upon that.

As for using fetch I have few concerns:

  • fetch could be used only in development ; in production we would need to be able to load the bundle from the filesystem/memory/something else. If we were to use fetch in dev and something else in prod, it would create a serious discrepancy. One way to work this out would be to allow fetch to load file:// URLs but this assumes that the files are stored on filesystem, which limits the usage. That's why we proposed to have a nice native API and let the user decide how they would want to load it. When doing remote debugging on chrome, we would probably use fetch though.
  • Event if fetch has an ability to load script from whatever we want it would need to send that data through the bridge and we would really want to avoid having to send few MBs of text through it. Additionally this poses some problems when it comes to fetching and sending RAM bundles - they are in binary format and on Android are splitted across multiple files.

Please correct me if I misunderstood the proposal.

As for the first point with bundling, there's already a quite a lot of work in bundling step. Even in original proposal and original PR, the native side didn't know anything except of the initial bundle. Everything else was done by JS or requested from JS. Right now, we would like to focus on getting the native side done, since we already have the bundling done in Haul for multi-bundle scenario.

@zamotany
Copy link
Author

@cpojer Is there any feedback from RN team on #127 (comment) and #127 (comment) ??

@kelset kelset closed this as completed Jun 10, 2021
@react-native-community react-native-community locked and limited conversation to collaborators Jun 10, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
🗣 Discussion This label identifies an ongoing discussion on a subject
Projects
None yet
Development

No branches or pull requests