-
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
Refactor vanishing point calculation to use EPSG:3857 projection #3661
Conversation
I"m not seeing the NavigationMapRoute.Builder#withVanishingRouteLineUpdateIntervalNano you refer to. |
I'm seeing a regression of this issue: #3615 |
Great work @LukasPaczos.
Yes, while it shouldn't hold back merging this improvement, it's not a solution that will scale well (or downscales ).
To be clear, if we can cut down the exeuction time, we would be able to update more frequently? To me it would result in a better UX but the issue of 1 component catching up to another will remain? or are the update in time for next render? do you have any concrete numbers on the cpu time? is this also an issue with a release build? |
@tobrun yes, definitely. Right now the native layer property setter is the bottlneck. Is there an opportunity to move more of the native internals of that method to a worker thread? |
46530b1
to
e560e0e
Compare
I didn't push the latest 🤦 |
e560e0e
to
6147ddb
Compare
No, the second gif from the OP represents no throttling in place, the vanishing point and the puck are perfectly in sync.
This is a capture from an unlocked, ~20 second run of a route from Poland to Spain, where the line's gradient is updated on each frame of the location puck's animation for maximum precision. ~60% of the main thread's time is spent in the native paint property setter and the app is stuttering a lot. Here's the full recording @tobrun: cpu-art-20201020T111444.zip
Nav SDK debug builds already use release builds of the Maps SDK so I wasn't expecting much improvement, but the performance is actually improved in release builds, probably optimizing some of the Nav SDK calculations. I increased the default interval to 16 updates per second which looks good even for a partially transparent puck, although I'd still like to be conservative with the number of updates we execute in the interest of battery consumption. I feel like the impl works great for the default scenario and each customer can fine-tune the update frequency to best match the use-case and devices' capabilities while we can work on other optimizations in parallel. |
69cdb8c
to
cae460c
Compare
Not able to reproduce with the latest changes, am I doing it correctly? |
e0b5d88
to
3b69582
Compare
3b69582
to
c016f9d
Compare
I suggest testing with the simulator so that the animation callback doesn't get called and the line updated too quickly to see the issue. I was able reproduce it several times. |
@cafesilencio would you be able to retest with the latest if you have a reproducible example handy? |
After pulling the latest I'm not able to reproduce the issue after trying several times. |
Thanks for retesting @cafesilencio! In this case, this is ready for another round. |
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.
Great work.
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.
Great work @LukasPaczos
Left some minor comments.
/** | ||
* Finds the count of remaining points in the current step. | ||
* | ||
* TurfMisc.lineSliceAlong places an additional point at index 0 to mark the precise | ||
* cut-off point which we can safely ignore. | ||
* We'll add the distance from the upcoming point to the current's puck position later. | ||
*/ | ||
allRemainingPoints += try { | ||
TurfMisc.lineSliceAlong( | ||
LineString.fromLngLats(currentStepProgress.stepPoints ?: emptyList()), | ||
currentStepProgress.distanceTraveled.toDouble(), | ||
currentStepProgress.step?.distance() ?: 0.0, | ||
TurfConstants.UNIT_METERS | ||
).coordinates().drop(1).size | ||
} catch (e: TurfException) { | ||
0 | ||
} |
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 about extracting this block of code into a private
fun
ction and give it a name based on the comment? This way updateUpcomingRoutePointIndex
will be easier to read and understand and comment will become superfluous so it won't be necessary.
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.
All those small pieces of logic have no application outside of this main method's context, they will not be reused anywhere else, and splitting them up would only artificially increase the method count of the library. In my opinion, the comments already split the impl into readable chunks.
The same applies to all comments below.
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'm gonna go ahead and merge, let's address later if needed.
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 don't agree. Normally comments are a code smell, they get outdated quickly because code changes but comments don't 😬 so they start lying 🤯
The best comment is a good name for a method or class.
or a variable.
Our future selves would thank us!
/** | ||
* Add to the count of remaining points all of the remaining points on the current leg, | ||
* after the current step. | ||
*/ | ||
val currentLegSteps = completeRoutePoints.nestedList[currentLegProgress.legIndex] | ||
allRemainingPoints += if (currentStepProgress.stepIndex < currentLegSteps.size) { | ||
currentLegSteps.slice( | ||
currentStepProgress.stepIndex + 1 until currentLegSteps.size - 1 | ||
).flatten().size | ||
} else { | ||
0 | ||
} |
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 re: extracting to a private
fun
ction
/** | ||
* Add to the count of remaining points all of the remaining legs. | ||
*/ | ||
for (i in currentLegProgress.legIndex + 1 until completeRoutePoints.nestedList.size) { | ||
allRemainingPoints += completeRoutePoints.nestedList[i].flatten().size | ||
} |
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 re: extracting to a private
fun
ction
/** | ||
* When we know the number of remaining points and the number of all points, | ||
* calculate the index of the upcoming point. | ||
*/ | ||
val allPoints = completeRoutePoints.flatList.size | ||
primaryRouteRemainingDistancesIndex = allPoints - allRemainingPoints - 1 | ||
} ?: run { primaryRouteRemainingDistancesIndex = null } |
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 re: extracting to a private
fun
ction
/** | ||
* Take the remaining distance from the upcoming point on the route and extends it | ||
* by the exact position of the puck. | ||
*/ | ||
val remainingDistance = | ||
traveledIndex.distanceRemaining + calculateDistance(upcomingPoint, point) |
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 about using a different naming (based on the comment) for the explaining variable? This way updateTraveledRouteLine
will be easier to read and understand and comment will become superfluous so it won't be necessary.
/** | ||
* Calculate the percentage of the route traveled and update the expression. | ||
*/ | ||
if (granularDistances.distance >= remainingDistance) { | ||
val offset = (1.0 - remainingDistance / granularDistances.distance) | ||
if (offset >= 0) { | ||
val expression = getExpressionAtOffset(offset) | ||
hideCasingLineAtOffset(offset) | ||
hideRouteLineAtOffset(offset) | ||
decorateRouteLine(expression) | ||
} |
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 about extracting this block of code into a private
fun
ction and give it a name based on the comment? This way updateTraveledRouteLine
will be easier to read and understand and comment will become superfluous so it won't be necessary.
for (i in (points.size - 1) downTo 1) { | ||
val curr = points[i] | ||
val prev = points[i - 1] | ||
distance += calculateDistance(curr, prev) | ||
indexArray.append(i - 1, RouteLineDistancesIndex(prev, distance)) | ||
} | ||
indexArray.append( | ||
points.size - 1, | ||
RouteLineDistancesIndex(points[points.size - 1], 0.0) | ||
) |
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 re: extracting to private
fun
ctions so it's easier to read and understand
/** | ||
* Calculates the distance between 2 points using | ||
* [EPSG:3857 projection](https://epsg.io/3857). | ||
* Info in [mapbox-gl-js/issues/9998](https://github.com/mapbox/mapbox-gl-js/issues/9998). | ||
*/ | ||
fun calculateDistance(point1: Point, point2: Point): Double { | ||
val d = doubleArrayOf( | ||
(projectX(point1.longitude()) - projectX(point2.longitude())), | ||
(projectY(point1.latitude()) - projectY(point2.latitude())) | ||
) | ||
return sqrt(d[0] * d[0] + d[1] * d[1]) | ||
} |
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.
Should we expose this from mapbox-java
?
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.
Interesting idea, let's track in mapbox/mapbox-java#1208 👍
This PR updates the algorithm that calculates the vanishing point of the route to use EPSG:3857 projection instead of turf measurements. The precision is now spot on, even on extremely long routes, it even greatly outperforms the precision of the over zoomed map tiles.
By dropping all of the required turf calculations, we also significantly improve the performance of the
MapRouteLine
. After spending a couple of cycles optimizing the code, we're at a point where the biggest penalty comes from callingLayer#nativeSetPaintProperty
which we won't be able to rule out from the Nav SDK level.In the ideal scenario, we're trying to update the lines' gradient expression on each frame of the puck. The gradient expression can become very large for long routes (especially with traffic congestion enabled) and unfortunately, the performance is not good enough for us to fully unlock the potential of the vanishing route line.
That's why I've opted to add a throttling mechanism for the vanishing point updates. It defaults to performing 5 updates per second, which lets the map remain perfectly smooth even for very long routes, while it does not impact the UX at all because our puck hides the imperfections:
You can see that I need to overzoom well beyond zoom level 18 (which the Nav experience is locked to by default) to notice the throttled animation.
That said, the precision is there, so if developers would need a smoother vanishing point animation, they can use
NavigationMapRoute.Builder#withVanishingRouteLineUpdateIntervalNano
:/cc @tmpsantos @tobrun for ideas how to optimize native paint property setter, this shouldn't be a blocker though