Status Update
Comments
ho...@gmail.com <ho...@gmail.com> #2
Background context:
Bundle
always returns an implicit default value for primitives but returns null for reference types.- Performance is a top priority, and achieving a "zero-cost abstraction" has been a key goal for
androidx.savedstate
in its transition to KMP, as outlined in go/bundle-kmp (design doc). - Requiring users to rely on the
Bundle
variant is not an option, as these APIs are designed specifically for KMP clients.
There are a few design options to address this problem if we consider the background context:
- Option 1: Prioritize API consistency while keeping the performance impact minimal -- but not zero.
- Pros:
- Ensures all APIs behave in a more type-safe and consistent manner.
- Cons:
- Increases inlined code from 2 to 11 LOC per method call.
- Additional method calls and checks in the worst-case scenario.
- Minimal impact in the best-case scenario.
- Implementation:
- This approach has been proposed by the reporter in aosp/3500556.
- Note: The benchmark in aosp/3500556 is compared against the current implementation, not option 2.
- Pros:
- Option 2: Give a step back, and focus on a true "zero-cost abstraction."
- Pros:
- Matches
Bundle
behavior: familiar for Java and Android developers. - Inlined code reduced to 1 LOC.
- There is zero performance runtime overhead.
- Matches
- Cons:
- Matches
Bundle
behavior: (may be) unfamiliar for other platforms. - Inconsistency between primitives and reference getters.
- Note:
null
values would result in anException
as we never return nullable types inSavedState
instead.
- Note:
- Matches
- Implementation:
- Ensure all methods directly call
Bundle
. - Remove any additional checks, including
. They (probably) shouldn't have been added, as they violate zero-cost abstraction.keyNotFoundError
- Ensure all methods directly call
- Pros:
We need to finalize a design decision before moving forward and document the outcome here.
ma...@gmail.com <ma...@gmail.com> #3
Thanks for the summary and background. I think we can also add option 3: combine 1 and 2 and provide, say:
- getXXX() // closest to Bundle
- getXXXOrThrow() // behaves like getXXX() in current implementation
- getXXXOrElse()
That "zero-cost abstraction" in the design doc reads more like it's referring to the types SavedStateReader/SavedStateWriter and less so for their functions. I consider our goal here is to make a "better" Bundle, and that probably includes (but not limited to):
- cross platform
- more Kotlin idiomatic (e.g. no
null
, naming closer to Kotlin's Map, etc.) - safer (e.g. extra checks to report mistakes)
- more sensible (e.g. List instead of ArrayList, more consistent, etc.)
Also some notes on "Minimal impact in the best-case scenario." for option 1. It's actually more like some impacts in different scenarios. Some good, some bad. Here are some ballpark numbers for getXXX
:
-
Bundle - 20ns
-
current implementation:
- has key
- has valid value - 40ns
- has no valid value - 40ns (return default value)
- has no key - 20ns (throw)
- aosp/3500556:
- has key
- has valid value
- value is equal to default - 40ns
- value is not equal to default - 20ns
- has no valid value - 60ns (throw)
- has valid value
- has no key - 60ns (throw)
Bolded situations are what I think the most likely situations.
ap...@google.com <ap...@google.com> #4
That "zero-cost abstraction" in the design doc reads more like it's referring to the types SavedStateReader/SavedStateWriter and less so for their functions.
To clarify, when I wrote that sentence, my focus was on:
- Avoiding unnecessary allocations (compared to
Bundle
). - Avoiding unnecessary work (compared to
Bundle
). - Minimizing inlined code (compared to
Bundle
).
I consider our goal here is to make a "better" Bundle, and that probably includes (but not limited to).
That's correct, and the goal also includes a low/zero-cost abstraction while maintaining backward compatibility with existing APIs.
About option 3, I would avoid this path as that means increasing the Public API surface.
Let's discuss offline tomorrow in our sync and document the outcome here.
jb...@google.com <jb...@google.com> #5
Adding more numbers. Testing the happy path (bundle has value of correct type) using Microbenchmark on a Pixel Fold with a small Bundle with 6 entries.
The candidats are:
- Bundle.getXXX - the overload without default value
- Bundle.getXXX(with default) - the overload with default value
- current - current implementation
- no collision - aosp/3500738 when we only need to call getXXX once
- collision - aosp/3500738 when we need to call getXXX twice
- no collision, no inline - aosp/3500738 when we only need to call getXXX once, but remove the inline
- collision, no inline - aosp/3500738 when we need to call getXXX twice, but remove the inline
Here's the result:
Bundle.getXXX | Bundle.getXXX(with default) | current | no collision | collision | no collision, no inline | collision, no inline | |
---|---|---|---|---|---|---|---|
getBoolean | 37.8 ns | 22.7 ns | 43.1 ns | 22.7 ns | 44.5 ns | 24.8 ns | 46.3 ns |
getChar | 38.5 ns | 23.2 ns | 44.8 ns | 23.6 ns | 46.0 ns | 24.8 ns | 47.5 ns |
getDouble | 37.0 ns | 22.4 ns | 42.8 ns | 22.7 ns | 45.8 ns | 23.7 ns | 46.3 ns |
getFloat | 38.5 ns | 23.2 ns | 43.1 ns | 23.0 ns | 46.0 ns | 24.2 ns | 47.7 ns |
getInt | 38.2 ns | 22.8 ns | 43.4 ns | 22.9 ns | 44.4 ns | 25.6 ns | 46.9 ns |
getLong | 38.6 ns | 23.3 ns | 43.9 ns | 23.9 ns | 45.2 ns | 24.9 ns | 47.0 ns |
Bundle.getXXX without default value is surprisingly slow. And inlining has a slight performance benefit.
Description
Component used: Navigation Version used: 2.9.0-alpha05 Devices/Android versions reproduced on: 33
I have two nested navHostController mainNavController dashboardNavController
in dashboardNavHost when i switch between tabs with this code :
dashboardNavController.navigate(rout) { popUpTo(dashboardNavController.graph.startDestinationId) { saveState = true } launchSingleTop = true restoreState = true }
and then with mainNavController go to other composable screen, when popup back, i get the following error:
java.lang.IllegalStateException: The saved state value associated with the key 'android-support-nav:controller:backStackIds' is either null or not of the expected type. This might happen if the value was saved with a different type or if the saved state has been modified unexpectedly. at androidx.navigation.NavController.restoreState(NavController.android.kt:3697) at androidx.navigation.compose.NavHostControllerKt$NavControllerSaver$2.invoke(NavHostController.kt:80) at androidx.navigation.compose.NavHostControllerKt$NavControllerSaver$2.invoke(NavHostController.kt:78) at androidx.compose.runtime.saveable.SaverKt$Saver$1.restore(Saver.kt:65) at androidx.compose.runtime.saveable.RememberSaveableKt.rememberSaveable(RememberSaveable.kt:86) at androidx.navigation.compose.NavHostControllerKt.rememberNavController(NavHostController.kt:59)