Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 1 | # Isolated Splits |
| 2 | |
| 3 | This doc aims to explain the ins and outs of using Isolated Splits on Android. |
| 4 | |
| 5 | For an overview of apk splits and how to use them in Chrome, see |
| 6 | [android_dynamic_feature_modules.md]. |
| 7 | |
| 8 | [TOC] |
| 9 | |
| 10 | ## About |
| 11 | |
| 12 | ### What are Isolated Splits? |
| 13 | |
| 14 | Isolated Splits is an opt-in feature (via [android:isolatedSplits] manifest |
| 15 | entry) that cause all feature splits in an application to have separate |
| 16 | `Context` objects, rather than being merged together into a single Application |
| 17 | `Context`. The `Context` objects have distict `ClassLoader` and `Resources` |
| 18 | instances. They are loaded on-demand instead of eagerly on launch. |
| 19 | |
| 20 | With Isolated Splits, each feature split is loaded in its own ClassLoader, with |
| 21 | the parent split set as the parent ClassLoader. |
| 22 | |
| 23 | [android:isolatedSplits]: https://developer.android.com/reference/android/R.attr#isolatedSplits |
| 24 | |
| 25 | ### Why use Isolated Splits? |
| 26 | |
| 27 | The more DEX that is loaded on start-up, the more RAM and time it takes for |
| 28 | application code to start running. Loading less code on start-up is particularly |
| 29 | helpful for Chrome, since Chrome tends to spawn a lot of processes, and because |
| 30 | renderer processes require almost no DEX. |
| 31 | |
| 32 | ### What Splits Exist in Chrome? |
| 33 | |
| 34 | Chrome's splits look like: |
| 35 | |
| 36 | ``` |
Clemens Arbesser | 46cb4f7 | 2022-12-07 15:08:17 | [diff] [blame] | 37 | base.apk <-- chrome.apk <-- image_editor.apk |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 38 | <-- feedv2.apk |
| 39 | <-- ... |
| 40 | ``` |
| 41 | |
| 42 | * The browser process loads the `chrome` split on start-up, and other splits are |
| 43 | loaded on-demand. |
| 44 | * Renderer and GPU processes do not load any feature splits. |
| 45 | * The `chrome` split exists to minimize the amount of DEX loaded by renderer |
| 46 | processes. However, it also enables faster browser process start-up by |
| 47 | allowing DEX to be loaded concurrently with other start-up tasks. |
| 48 | |
| 49 | ### How are Isolated Splits Loaded? |
| 50 | |
| 51 | There are two ways: |
| 52 | 1) They can be loaded by Android Framework when handling an intent. |
| 53 | * E.g.: If a feature split defines an Activity in its manifest, Android |
| 54 | will create the split's Context and associate the Activity with it. |
| 55 | 2) They can be loaded explicitly via [BundleUtils.createIsolatedSplitContext()]. |
| 56 | * The most common way to load in this way is through declaring a |
| 57 | `ModuleInterface`, as described in [android_dynamic_feature_modules.md]. |
| 58 | |
| 59 | [BundleUtils.createIsolatedSplitContext()]: https://source.chromium.org/search?q=func:createIsolatedSplitContext&ss=chromium |
| 60 | [android_dynamic_feature_modules.md]: android_dynamic_feature_modules.md |
| 61 | |
| 62 | ## OS Support for Isolated Splits |
| 63 | |
| 64 | Initial support was added in Android O. On earlier Android versions, all |
| 65 | feature splits are loaded during process start-up and merged into the |
| 66 | Application Context. |
| 67 | |
| 68 | ## OS Bugs |
| 69 | |
| 70 | ### Base ClassLoader used for Services in Splits (Android Pre-S) |
| 71 | |
| 72 | Service Contexts are created with the base split's ClassLoader rather than the |
| 73 | split's ClassLoader. |
| 74 | |
| 75 | Fixed in Android S. Bug: [b/169196314] (Googler only). |
| 76 | |
| 77 | **Work-around:** |
| 78 | |
| 79 | We use [SplitCompatService] (and siblings) to put a minimal service class in the |
| 80 | base split. They forward all calls to an implementation class, which can live |
| 81 | in the `chrome` split (or other splits). We also have a [compile-time check] to |
| 82 | enforce that no Service subclasses exist outside of the base split. |
| 83 | |
| 84 | [b/169196314]: https://issuetracker.google.com/169196314 |
| 85 | [SplitCompatService]: https://source.chromium.org/search?q=symbol:SplitCompatService&ss=chromium |
Sam Maier | 18745fa | 2024-12-03 16:26:04 | [diff] [blame] | 86 | [compile-time check]: https://source.chromium.org/chromium/chromium/src/+/main:build/android/gyp/create_app_bundle.py;l=446;drc=c4dd266492ad1e242161b415ac5a1d9fccd7a041 |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 87 | |
| 88 | ### Corrupted .odex (Android O MR1) |
| 89 | |
| 90 | Android O MR1 has a bug where `bg-dexopt-job` (runs during maintenance windows) |
| 91 | breaks optimized dex files for Isolated Splits. The corrupt `.odex` files cause |
| 92 | extremely slow startup times. |
| 93 | |
| 94 | **Work-around:** |
| 95 | |
| 96 | We [preemptively run] `dexopt` so that `bg-dexopt-job` decides there is no work |
| 97 | to do. We trigger this from [PackageReplacedBroadcastReceiver] so that it |
| 98 | happens whenever Chrome is updated rather than when the user launches Chrome. |
| 99 | |
| 100 | [preemptively run]: https://source.chromium.org/search?q=symbol:DexFixer.needsDexCompile&ss=chromium |
| 101 | [PackageReplacedBroadcastReceiver]: https://source.chromium.org/search?q=symbol:PackageReplacedBroadcastReceiver&ss=chromium |
| 102 | |
Andrew Grieve | 10c6c56 | 2024-08-01 18:36:14 | [diff] [blame] | 103 | ### Conflicting ClassLoaders #1 |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 104 | |
Andrew Grieve | fdc8302 | 2023-03-24 16:49:57 | [diff] [blame] | 105 | Tracked by [b/172602571], sometimes a split's parent ClassLoader is different |
| 106 | from the Application's ClassLoader. This manifests as odd-looking |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 107 | `ClassCastExceptions` where `"TypeA cannot be cast to TypeA"` (since the two |
| 108 | `TypeAs` are from different ClassLoaders). |
| 109 | |
Andrew Grieve | b93843f | 2022-06-21 15:08:37 | [diff] [blame] | 110 | Tracked by UMA `Android.IsolatedSplits.ClassLoaderReplaced`. Occurs < 0.05% of |
| 111 | the time. |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 112 | |
| 113 | **Work-around:** |
| 114 | |
| 115 | On Android O, there is no work-around. We just [detect and crash early]. |
| 116 | |
| 117 | Android P added [AppComponentFactory], which offers a hook that we use to |
| 118 | [detect and fix] ClassLoader mixups. The ClassLoader mixup also needs to be |
| 119 | corrected for `ContextImpl` instances, which we do via |
| 120 | [ChromeBaseAppCompatActivity.attachBaseContext()]. |
| 121 | |
| 122 | [b/172602571]: https://issuetracker.google.com/172602571 |
| 123 | [detect and crash early]: https://source.chromium.org/search?q=crbug.com%2F1146745&ss=chromium |
| 124 | [AppComponentFactory]: https://developer.android.com/reference/android/app/AppComponentFactory |
| 125 | [detect and fix]: https://source.chromium.org/search?q=f:splitcompatappcomponentfactory&ss=chromium |
| 126 | [ChromeBaseAppCompatActivity.attachBaseContext()]: https://source.chromium.org/search?q=BundleUtils\.checkContextClassLoader&ss=chromium |
| 127 | |
Andrew Grieve | 10c6c56 | 2024-08-01 18:36:14 | [diff] [blame] | 128 | ### Conflicting ClassLoaders #2 |
| 129 | |
| 130 | Tracked by [b/172602571], when a new split language split or feature split is |
| 131 | installed, the ClassLoaders for non-base splits are recreated. Any reference to |
| 132 | a class from the previous ClassLoader (e.g. due to native code holding |
| 133 | references to them) will result in `ClassCastExceptions` where |
| 134 | `"TypeA cannot be cast to TypeA"`. |
| 135 | |
| 136 | **Work-around:** |
| 137 | |
| 138 | There is no work-around. This is a source of crashes. We could potentially |
| 139 | mitigate by restarting chrome when a split is installed. |
| 140 | |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 141 | ### System.loadLibrary() Broken for Libraries in Splits |
| 142 | |
| 143 | Tracked by [b/171269960], Android is not adding the apk split to the associated |
| 144 | ClassLoader's `nativeSearchPath`. This means that `libfoo.so` within an |
| 145 | isolated split is not found by a call to `System.loadLibrary("foo")`. |
| 146 | |
| 147 | **Work-around:** |
| 148 | |
| 149 | Load libraries via `System.load()` instead. |
| 150 | |
| 151 | ```java |
| 152 | System.load(BundleUtils.getNativeLibraryPath("foo", "mysplitsname")); |
| 153 | ``` |
| 154 | |
| 155 | [b/171269960]: https://issuetracker.google.com/171269960 |
| 156 | |
Andrew Grieve | 89a28b1 | 2022-12-02 19:57:22 | [diff] [blame] | 157 | ### System.loadLibrary() Unusable from Split if Library depends on Another Loaded by Base Split |
| 158 | |
| 159 | Also tracked by [b/171269960], maybe related to linker namespaces. If a split |
| 160 | tries to load `libfeature.so`, and `libfeature.so` has a `DT_NEEDED` entry for |
| 161 | `libbase.so`, and `libbase.so` is loaded by the base split, then the load will |
| 162 | fail. |
| 163 | |
| 164 | **Work-around:** |
| 165 | |
| 166 | Have base split load libraries from within splits. Proxy all JNI calls through |
| 167 | a class that exists in the base split. |
| 168 | |
Andrew Grieve | e7a8cdf | 2022-05-16 17:46:11 | [diff] [blame] | 169 | ### System.loadLibrary() Broken for Libraries in Splits on System Image |
| 170 | |
| 171 | Also tracked by [b/171269960], Android's linker config (`ld.config.txt`) sets |
| 172 | `permitted_paths="/data:/mnt/expand"`, and then adds the app's `.apk` to an |
| 173 | allowlist. This allowlist does not contain apk splits, so library loading is |
| 174 | blocked by `permitted_paths` when the splits live on the `/system` partition. |
| 175 | |
| 176 | **Work-around:** |
| 177 | |
| 178 | Use compressed system image stubs (`.apk.gz` and `-Stub.apk`) so that Chrome is |
| 179 | extracted to the `/data` partition upon boot. |
| 180 | |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 181 | ### Too Many Splits Break App Zygote |
| 182 | |
| 183 | Starting with Android Q / TriChrome, Chrome uses an [Application Zygote]. As |
| 184 | part of initialization, Chrome's `ApplicationInfo` object is serialized into a |
| 185 | fixed size buffer. Each installed split increases the size of the |
| 186 | `ApplicationInfo` object, and can push it over the buffer's limit. |
| 187 | |
| 188 | **Work-around:** |
| 189 | |
| 190 | Do not add too many splits, and monitor the size of our `ApplicationInfo` object |
| 191 | ([crbug/1298496]). |
| 192 | |
| 193 | [crbug/1298496]: https://bugs.chromium.org/p/chromium/issues/detail?id=1298496 |
| 194 | [Application Zygote]: https://developer.android.com/reference/android/app/ZygotePreload |
| 195 | |
Andrew Grieve | fdc8302 | 2023-03-24 16:49:57 | [diff] [blame] | 196 | ### AppComponentFactory does not Hook Split ClassLoaders |
| 197 | |
| 198 | `AppComponentFactory#instantiateClassLoader()` is meant to allow apps to hook |
| 199 | `ClassLoader` creation. The hook is called for the base split, but not for other |
| 200 | isolated splits. Tracked by [b/265583114]. There is no work-around. |
| 201 | |
| 202 | [b/265583114]: https://issuetracker.google.com/265583114 |
| 203 | |
| 204 | ### Incorrect Handling of Shared Libraries |
| 205 | |
| 206 | Tracked by [b/265589431]. If an APK split has `<uses-library>` in its manifest, |
| 207 | the classloader for the split is meant to have that library added to it by the |
| 208 | framework. However, Android does not add the library to the classpath when a |
| 209 | split is dynamically installed, but instead adds it to the classpath of the base |
| 210 | split's classloader upon subsequent app launches. |
| 211 | |
| 212 | **Work-around:** |
| 213 | |
Andrew Grieve | 6d44e66e | 2024-07-26 14:24:12 | [diff] [blame] | 214 | * Always add `<uses-library>` to the base split. |
| 215 | |
| 216 | [b/265589431]: https://issuetracker.google.com/265589431 |
Andrew Grieve | fdc8302 | 2023-03-24 16:49:57 | [diff] [blame] | 217 | |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 218 | ## Other Quirks & Subtleties |
| 219 | |
| 220 | ### System Image APKs |
| 221 | |
| 222 | When distributing Chrome on Android system images, we generate a single `.apk` |
| 223 | file that contains all splits merged together (or rather, all splits whose |
Andrew Grieve | 5f71ed62 | 2022-04-28 13:57:38 | [diff] [blame] | 224 | `AndroidManifest.xml` contain `<dist:fusing dist:include="true" />`). We do this |
| 225 | for simplicity; Android supports apk splits on the system image. |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 226 | |
Andrew Grieve | 5f71ed62 | 2022-04-28 13:57:38 | [diff] [blame] | 227 | You can build Chrome's system `.apk` via: |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 228 | ```sh |
Andrew Grieve | 5f71ed62 | 2022-04-28 13:57:38 | [diff] [blame] | 229 | out/Release/bin/trichrome_chrome_bundle build-bundle-apks --output-apks SystemChrome.apks --build-mode system |
| 230 | unzip SystemChrome.apks system/system.apk |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 231 | ``` |
| 232 | |
| 233 | Shipping a single `.apk` file simplifies distribution, but eliminates all the |
| 234 | benefits of Isolated Splits. |
| 235 | |
| 236 | ### Chrome's Application ClassLoader |
| 237 | |
| 238 | A lot of Chrome's code uses the `ContextUtils.getApplicationContext()` as a |
| 239 | Context object. Rather than auditing all usages and replacing applicable ones |
| 240 | with the `chrome` split's Context, we [use reflection] to change the |
| 241 | Application instance's ClassLoader to point to the `chrome` split's ClassLoader. |
| 242 | |
| 243 | [use reflection]: https://source.chromium.org/search?q=f:SplitChromeApplication%20replaceClassLoader&ss=chromium |
| 244 | |
| 245 | ### ContentProviders |
| 246 | |
| 247 | Unlike other application components, ContentProviders are created on start-up |
| 248 | even when they are not the reason the process is being created. If a |
| 249 | ContentProvider were to be declared in a split, its split's Context would need |
| 250 | to be loaded during process creation, eliminating any benefit. |
| 251 | |
| 252 | **Work-around:** |
| 253 | |
| 254 | We declare all ContentProviders in the base split's `AndroidManifest.xml` and |
| 255 | enforce this with a [compile-time check]. ContentProviders that would pull in |
| 256 | significant amounts of code use [SplitCompatContentProvider] to delegate to a |
| 257 | helper class living within a split. |
| 258 | |
| 259 | [compile-time check]: https://source.chromium.org/search?q=symbol:_MaybeCheckServicesAndProvidersPresentInBase&ss=chromium |
| 260 | [SplitCompatContentProvider]: https://source.chromium.org/search?q=symbol:SplitCompatContentProvider&ss=chromium |
| 261 | |
| 262 | ### JNI and ClassLoaders |
| 263 | |
| 264 | When you call from native->Java (via `@CalledByNative`), there are two APIs |
| 265 | that Chrome could use to resolve the target class: |
| 266 | |
| 267 | 1) JNI API: [JNIEnv::FindClass()] |
| 268 | 2) Java Reflection API:`ClassLoader.loadClass())` |
| 269 | |
| 270 | Chrome uses #2. For methods within feature splits, `generate_jni()` targets |
| 271 | use `split_name = "foo"` to make the generated JNI code use the split's |
| 272 | ClassLoader. |
| 273 | |
| 274 | [JNIEnv::FindClass()]: https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html#wp16027 |
| 275 | |
| 276 | ### Accessing Android Resources |
| 277 | |
| 278 | When resources live in a split, they must be accessed through a Context object |
| 279 | associated with that split. However: |
| 280 | |
| 281 | * Bug: Chrome's build system [improperly handles ID conflicts] between splits. |
| 282 | * Bug: Splash screens [fail to load] for activities in Isolated Splits (unless |
| 283 | associated resources are defined in the base split). |
| 284 | * Quirk: `RemoteViews`, notification icons, and other Android features that |
| 285 | access resources by Package ID require resources to be in the base split when |
| 286 | Isolated Splits are enabled. |
| 287 | |
| 288 | **Work-around:** |
| 289 | |
| 290 | Chrome [stores all Android resources in the base split]. There is [a crbug] to |
| 291 | track moving resources into splits, but it may prove too challenging. |
| 292 | |
| 293 | [stores all Android resources in the base split]: https://source.chromium.org/search?q=recursive_resource_deps%5C%20%3D%5C%20true |
| 294 | [improperly handles ID conflicts]: https://crbug.com/1133898 |
| 295 | [fail to load]: https://issuetracker.google.com/171743801 |
| 296 | [a crbug]: https://crbug.com/1165782 |
| 297 | |
| 298 | ### Inflating Layouts |
| 299 | |
| 300 | Layouts should be inflated with an Activity Context so that |
| 301 | configuration-specific resources and themes are used. If layouts contain |
| 302 | references to View classes from different feature splits than the Activity's, |
| 303 | then the views' split ClassLoaders must be used. |
| 304 | |
| 305 | **Work-around:** |
| 306 | |
| 307 | Use the `ContextWrapper` created via: [BundleUtils.createContextForInflation()] |
| 308 | |
| 309 | [BundleUtils.createContextForInflation()]: https://source.chromium.org/search?q=symbol:BundleUtils.createContextForInflation&ss=chromium |
| 310 | |
Andrew Grieve | 9439213f | 2022-11-23 16:30:32 | [diff] [blame] | 311 | ### onRestoreInstanceState with Classes From Splits |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 312 | |
| 313 | When Android kills an app, it normally calls `onSaveInstanceState()` to allow |
| 314 | the app to first save state. The saved state includes the class names of active |
Andrew Grieve | 9439213f | 2022-11-23 16:30:32 | [diff] [blame] | 315 | Fragments, RecyclerViews, and potentially other classes from splits. Upon |
| 316 | re-launch, these class names are used to reflectively instantiate instances. |
| 317 | `FragmentManager` uses the ClassLoader of the Activity to instantiate them, |
| 318 | and `RecyclerView` uses the ClassLoader associated with the `Bundle` object. |
| 319 | The reflection fails if the active Activity resides in a different spilt from |
| 320 | the reflectively instantiated classes. |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 321 | |
| 322 | **Work-around:** |
| 323 | |
| 324 | Chrome stores the list of all splits that have been used for inflation during |
Andrew Grieve | 9439213f | 2022-11-23 16:30:32 | [diff] [blame] | 325 | [`onSaveInstanceState`] and then uses [a custom ClassLoader] to look within them |
| 326 | for classes that do not exist in the application's ClassLoader. The custom |
| 327 | ClassLoader is passed to `Bundle` instances in |
| 328 | `ChromeBaseAppCompatActivity.onRestoreInstanceState()`. |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 329 | |
Andrew Grieve | 87c12e0 | 2022-11-28 15:19:01 | [diff] [blame] | 330 | Having Android Framework call `Bundle.setClassLoader()` is tracked in |
| 331 | [b/260574161]. |
| 332 | |
Andrew Grieve | 9439213f | 2022-11-23 16:30:32 | [diff] [blame] | 333 | [`onSaveInstanceState`]: https://source.chromium.org/search?q=symbol:ChromeBaseAppCompatActivity.onSaveInstanceState&ss=chromium |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 334 | [a custom ClassLoader]: https://source.chromium.org/search?q=symbol:ChromeBaseAppCompatActivity.getClassLoader&ss=chromium |
Andrew Grieve | 87c12e0 | 2022-11-28 15:19:01 | [diff] [blame] | 335 | [b/260574161]: https://issuetracker.google.com/260574161 |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 336 | |
Andrew Grieve | 638a46a | 2025-03-03 20:58:20 | [diff] [blame] | 337 | ### Package Private Methods |
Sam Maier | 05e341ad | 2022-04-28 15:52:08 | [diff] [blame] | 338 | |
| 339 | Due to having different ClassLoaders, package-private methods don't work across |
Andrew Grieve | 638a46a | 2025-03-03 20:58:20 | [diff] [blame] | 340 | the boundary, even though they will compile. Release builds will fail during |
| 341 | the R8 step, which has a check for cross-split package-private access. |
Sam Maier | 05e341ad | 2022-04-28 15:52:08 | [diff] [blame] | 342 | |
| 343 | **Work around:** |
| 344 | |
| 345 | Make any method public that you wish to call in another module, even if it's in |
| 346 | the same package. |
| 347 | |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 348 | ### Proguarding Splits |
| 349 | |
| 350 | "Proguarding" is the build step that performs whole-program optimization of Java |
| 351 | code, and "R8" is the program Chrome uses to do this. R8 currently supports |
| 352 | mapping input `.jar` files to output feature splits. If two feature splits share |
| 353 | a common GN `dep`, then its associated `.jar` will be promoted to the parent |
| 354 | split (or to the base split) by our [proguard.py] wrapper script. |
| 355 | |
| 356 | This scheme means that if a single class from a large library is needed by, or |
| 357 | promoted to, the base split, then every class needed from that library by |
| 358 | feature splits will also remain in the base split. The feature request to have |
| 359 | R8 move code into deeper splits on a per-class basis is [b/225876019] (Googler |
| 360 | only). |
| 361 | |
| 362 | [proguard.py]: https://source.chromium.org/search?q=symbol:_DeDupeInputJars%20f:proguard.py&ss=chromium |
| 363 | [b/225876019]: https://issuetracker.google.com/225876019 |
| 364 | |
Andrew Grieve | fdc8302 | 2023-03-24 16:49:57 | [diff] [blame] | 365 | ### Metadata in Splits |
| 366 | |
| 367 | Metadata is queried on a per-app basis (not a per-split basis). E.g.: |
| 368 | |
| 369 | ```java |
| 370 | ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); |
| 371 | Bundle b = ai.metaData; |
| 372 | ``` |
| 373 | |
| 374 | This bundle contains merged values from all fully-installed apk splits. |
| 375 | |
Andrew Grieve | 520e090 | 2022-04-27 13:19:25 | [diff] [blame] | 376 | ## Other Resources |
| 377 | |
| 378 | * [go/isolated-splits-dev-guide] (Googlers only). |
| 379 | * [go/clank-isolated-splits-architecture] (Googlers only). |
| 380 | |
| 381 | [go/isolated-splits-dev-guide]: http://go/isolated-splits-dev-guide |
| 382 | [go/clank-isolated-splits-architecture]: http://go/clank-isolated-splits-architecture |