-
Notifications
You must be signed in to change notification settings - Fork 319
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
Add dynamic map FPS adjustment for NavigationMapboxMap #1669
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1669 +/- ##
============================================
+ Coverage 26.3% 26.48% +0.18%
- Complexity 790 815 +25
============================================
Files 197 202 +5
Lines 8345 8513 +168
Branches 598 623 +25
============================================
+ Hits 2195 2255 +60
- Misses 5952 6046 +94
- Partials 198 212 +14 |
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.
Left some minor comments. Other than that this looks good to me 👍
} | ||
|
||
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, DEFAULT_BATTERY_LEVEL); | ||
final boolean pluggedUsb = chargePlug == BatteryManager.BATTERY_PLUGGED_USB; |
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.
Why are these variables final
? Those are local to isPluggedIn
.
private static final int PLUGGED_IN_MAX_FPS = 120; | ||
static final int DEFAULT_MAX_FPS = 20; | ||
|
||
private final WeakReference<MapView> mapViewWeakReference; |
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.
Does this actually need to be a WeakReference
?
private static final int VALID_DURATION_IN_SECONDS_UNTIL_NEXT_MANEUVER = 7; | ||
private static final int VALID_DURATION_IN_SECONDS_SINCE_PREVIOUS_MANEUVER = 3; | ||
private static final int PLUGGED_IN_MAX_FPS = 120; | ||
static final int DEFAULT_MAX_FPS = 20; |
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.
Does this actually need to be package-private?
} else if (validLowFpsManeuver(routeLegProgress) || validLowFpsDuration(routeLegProgress)) { | ||
return maxFps; | ||
} else { | ||
return PLUGGED_IN_MAX_FPS; |
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.
For clarity, what about creating a separate constant called DEVICE_MAX_FPS
?
class MapFpsDelegate { | ||
|
||
private static final int VALID_DURATION_IN_SECONDS_UNTIL_NEXT_MANEUVER = 7; | ||
private static final int VALID_DURATION_IN_SECONDS_SINCE_PREVIOUS_MANEUVER = 5; |
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.
As measurements, durations are usually floating-point numbers. For example, in the iOS version, durations are typed as TimeInterval
, which is a type alias for Double
.
|
||
private static final int VALID_DURATION_IN_SECONDS_UNTIL_NEXT_MANEUVER = 7; | ||
private static final int VALID_DURATION_IN_SECONDS_SINCE_PREVIOUS_MANEUVER = 5; | ||
private static final int DEVICE_MAX_FPS = 120; |
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.
This should be set to the maximum frame rate that the device can achieve reliably.
On iOS, 120 is the refresh rate, and therefore the maximum recommended frame rate, of specific iPad Pro models. The iOS map SDK interprets the MGLMapViewPreferredFramesPerSecond.maximum
enumeration case to mean the device’s refresh rate, either 60 fps or 120 fps depending on the model, except on older devices that can’t handle 60 fps reliably, in which case it throttles back to 30 fps.
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.
This should be set to the maximum frame rate that the device can achieve reliably.
I don't feel like it's achievable to establish that based on a vast Android devices pool. I'd rather make this constants Integer.MAX_VALUE
and let the hardware output frames as fast as it can. I'm sure there are, or will be in the future, devices that will be able to push more than 120 frames per second.
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.
Yeah I couldn't find a simple way to get the device FPS capability - setting to Integer.MAX_VALUE
makes sense, as it will scale as Android screens get faster 😄
Thank you both for this feedback 💯
RouteLegProgress routeLegProgress = routeProgress.currentLegProgress(); | ||
|
||
if (isPluggedIn) { | ||
return DEVICE_MAX_FPS; |
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.
On iOS, when the device is plugged in, we use MGLMapViewPreferredFramesPerSecond.lowPower
, which is 30 fps. That’s equivalent to the frame rate on legacy devices – higher than when unplugged, but lower than when shouldPositionCourseViewFrameByFrame
is true (i.e., right before or after a maneuver).
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.
@1ec5 Yeah sorry, I was going to double check what this meant in the iOS code.
shouldPositionCourseViewFrameByFrame
is a boolean to say "I don't want to throttle anything - use device capability (FrameIntervalOptions.defaultFramesPerSecond
)"?
} else if (validLowFpsManeuver(routeLegProgress) || validLowFpsDuration(routeLegProgress)) { | ||
return maxFpsThreshold; | ||
} else { | ||
return DEVICE_MAX_FPS; |
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 also use MGLMapViewPreferredFramesPerSecond.lowPower
in this case on iOS.
|
||
private static final int VALID_DURATION_IN_SECONDS_UNTIL_NEXT_MANEUVER = 7; | ||
private static final int VALID_DURATION_IN_SECONDS_SINCE_PREVIOUS_MANEUVER = 5; | ||
private static final int DEVICE_MAX_FPS = 120; |
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.
This should be set to the maximum frame rate that the device can achieve reliably.
I don't feel like it's achievable to establish that based on a vast Android devices pool. I'd rather make this constants Integer.MAX_VALUE
and let the hardware output frames as fast as it can. I'm sure there are, or will be in the future, devices that will be able to push more than 120 frames per second.
} | ||
|
||
int maxFps = determineMaxFpsFrom(routeProgress, mapView.getContext()); | ||
if (currentFpsThreshold != maxFps) { |
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.
mapView.setMaximumFps(maxFps)
doesn't wire through any JNI and doesn't perform any logic, so we can drop currentFpsThreshold
check (and the field). This will make the code a tiny bit cleaner.
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.
awesome thanks for the heads up on that @LukasPaczos
@@ -286,6 +316,7 @@ public NavigationCamera retrieveCamera() { | |||
*/ | |||
public void updateCameraTrackingMode(@NavigationCamera.TrackingMode int trackingMode) { | |||
mapCamera.updateCameraTrackingMode(trackingMode); | |||
mapFpsDelegate.updateCameraTracking(trackingMode); |
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.
It seems like the SDK could make use of something along the lines of NavigationCamera#StateListener
which would broadcast camera mode updates. Not only it could be exposed, but also implemented internally by the likes of MapFpsDelegate
.
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.
@LukasPaczos good idea, I added this with the latest code.
4f2cffe
to
df33f66
Compare
Updated with tests and ready for another round here - thanks for the great feedback @Guardiola31337 @LukasPaczos @1ec5 @1ec5 If the answer to my question is yes, I think |
79259fe
to
1d4ea7e
Compare
@1ec5 with the @LukasPaczos what do you think about the ☝️ @1ec5 this also takes care of transitions between overview to tracking / tracking broken to tracking, thanks for pointing that out in scrum today. |
3e61876
to
5590793
Compare
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.
what do you think about the
TrackingModeTransitionListener
?
Unfortunately, relying only on the OnLocationCameraTransitionListener
for broadcasting CameraMode
changes can fail to convey the message during the transition. If we'd like to verify current camera mode based on the TrackingModeTransitionListener
we can run into a situation when it's not yet broadcasted because the transition hasn't finished, but the mode is actually already engaged.
Is there another way to check the current camera mode?
@@ -47,6 +49,7 @@ | |||
public class NavigationCamera implements LifecycleObserver { | |||
|
|||
private static final int ONE_POINT = 1; | |||
private final Set<TrackingModeTransitionListener> trackingModeTransitionListeners = new HashSet<>(); |
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.
I'd suggest using a CopyOnWriteArrayList
over here so that a user can call removeTrackingModeTransitionListener
from inside of the TrackingModeTransitionListener
callback without running into ConcurrentModificationException
.
|
||
private void initializeFpsDelegate(MapView mapView) { | ||
MapBatteryMonitor batteryMonitor = new MapBatteryMonitor(); | ||
mapFpsDelegate = new MapFpsDelegate(mapView, batteryMonitor); |
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.
MapFpsDelegate
can implement TrackingModeTransitionListener
directly instead of the nested FpsDelegateTrackingChangedListener
and can be added to/removed from the mapCamera
's listeners list in NavigationMapboxMap
's onStart
/onStop
. This will decrease the coupling between MapFpsDelegate
and NavigationCamera
as one will not depend on the other anymore.
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.
I believe that the same reasoning can be applied to the FpsDelegateProgressChangeListener
.
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.
This is a great recommendation - no need to create internal listeners as the class is package-private. For now, I only reworked the MapFpsDelegate
/ NavigationCamera
.
I believe that the same reasoning can be applied to the
FpsDelegateProgressChangeListener
.
☝️ This requires some bigger API rework (as most of the classes in NavigationMapboxMap
follow the same setup) that I'd like to revisit in a later PR.
382318d
to
a779df5
Compare
@@ -167,7 +159,7 @@ public void resume(Location location) { | |||
*/ | |||
public void updateCameraTrackingMode(@TrackingMode int trackingMode) { | |||
trackingCameraMode = trackingMode; |
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.
This should be set after we've evaluated that the passed trackingMode
is acceptable in setCameraMode
.
* | ||
* @param listener to be added | ||
*/ | ||
public void addOnTrackingModeTransitionListener(OnTrackingModeTransitionListener listener) { |
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.
NIT listener
s can be annotated with @NoNull
if (mode != locationComponent.getCameraMode()) { | ||
locationComponent.setCameraMode(mode); | ||
locationComponent.setCameraMode(mode, new NavigationCameraTransitionListener(this)); |
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 could use a private NavigationCameraTransitionListener
field instead of creating a new instance with every mode change.
...n-ui/src/main/java/com/mapbox/services/android/navigation/ui/v5/camera/NavigationCamera.java
Outdated
Show resolved
Hide resolved
@@ -346,8 +390,16 @@ private void setCameraMode() { | |||
Timber.e("Using unsupported camera tracking mode - %d.", trackingCameraMode); |
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.
trackingCameraMode = NAVIGATION_TRACKING_MODE_NONE
reassingment should be done here.
@Override | ||
public void onTransitionFinished(int trackingMode) { | ||
updateCameraTracking(trackingMode); | ||
resetMaxFps(!isTracking); |
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.
This invocation can be moved into #updateCameraTracking
.
@Override | ||
public void onTransitionCancelled(int trackingMode) { | ||
updateCameraTracking(trackingMode); | ||
resetMaxFps(!isTracking); |
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.
Same as above.
@@ -142,6 +148,28 @@ public void updateLocation(Location location) { | |||
updateMapWaynameWithLocation(location); | |||
} | |||
|
|||
/** | |||
* The maximum preferred frames per second at which to render the map. |
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.
It'd be great to mention that throttling is only going to be imposed only in camera tracking modes.
} | ||
|
||
/** | ||
* Enabled by default, the navigation map will throttle frames per second when the application has |
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.
Same as above.
|
||
@Override | ||
public void onLocationCameraTransitionCanceled(int cameraMode) { | ||
camera.updateTransitionListenersCancelled(); |
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.
The cameraMode
needs to be pushed to the internal listener and then pushed to the public listeners, otherwise, once canceled, the OnTrackingModeTransitionListener#onTransitionCancelled(@NavigationCamera.TrackingMode int trackingMode)
is going to deliver trackingMode
that canceled this transition (and is now transitioning itself) rather than the one that scheduled this transition, because the NavigationCamera#trackingCameraMode
is already going to be overwritten.
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.
And since those callbacks are posted to the back of the message queue in the Maps SDK, it'd apply the same logic to the camera.updateTransitionListenersFinished();
above.
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.
Minor changes requested, otherwise, looks great! Awesome work @danesfeder 🚀
* This property only takes effect when the application has limited resources, such as when | ||
* the device is running on battery power. By default, this is set to 20fps. | ||
* <p> | ||
* Throttling will also only take effect when the map is not currently tracking |
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.
map is not currently tracking
-> camera is currently tracking
* Enabled by default, the navigation map will throttle frames per second when the application has | ||
* limited resources, such as when the device is running on battery power. | ||
* <p> | ||
* Throttling will also only take effect when the map is not currently tracking |
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.
Same as above.
|
||
delegate.onTransitionFinished(NavigationCamera.NAVIGATION_TRACKING_MODE_NONE); | ||
|
||
verify(mapView).setMaximumFps(anyInt()); |
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.
This and all cases that use anyInt()
in this test class should more strictly verify that we are passing the right constant.
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.
Had the chance to run some performance tests and these are the numbers 🎉📈🎉 Results were obtained after running the same (automated) navigation session / route 30 times with and without OP's fix in a Nexus 5 👀 Power useAverage improvement 👉 3.003333333 mAh CPU usageAverage improvement 👉 26.68965517% Note 👇 for understanding percentages greater than 100% shown in the graph
|
This adds dynamic FPS throttling based on
RouteProgress
along the route. If we are going to execute a maneuver that is "lower risk" (continue straight, slight right, slight left) or we have determined we are far enough along the step (duration thresholds), we can set a lower ceiling for maximum FPS. iOS logicTODO:
cc @1ec5