RUM-7191 Implemented the basic logic for time-to-network-settle view metric#2392
Conversation
c759a92 to
90641b0
Compare
| private val resourceStartedTimestamps = HashMap<String, HashMap<String, Long>>() | ||
| private val resourceStoppedTimestamps = HashMap<String, PriorityQueue<Long>>() | ||
| private val viewCreatedTimestampsByBViewId = HashMap<String, Long>() |
There was a problem hiding this comment.
suggestion/ We need these maps because of using single instance of VNSMetric for the entire RUM application. If we instead create a distinct one for each RUM view scope there will be no need for using hash maps, passing view ID all around and likely the purge() routine will be gone. Eventually it can simplify this implementation.
Considering data flow and the domain of this problem (individual view metric), do we gain anything by sharing the same VNSMetric instance with all view scopes?
There was a problem hiding this comment.
you are totally right...I think there's nothing we can win here at least in this case. For the other metric we will have to share it so I am wondering if I should do it differently for this one ? What do you think ? It makes sense to remove the complexity though.
There was a problem hiding this comment.
One of the other reasons I wanted to share it was to not have to instantiate this all the time but I guess the memory penalty is not that high. And we will win on one side (not having multiple instances) but loose on the other that I will have to keep these maps and purge them from time to time.
| val startTimestamp = resourceStartedTimestamps[context.viewId]?.get(context.resourceId) | ||
| if (startTimestamp != null) { | ||
| val resourceSettleDuration = eventTimestampInNanos - startTimestamp | ||
| resourceStoppedTimestamps.getOrPut(context.viewId) { PriorityQueue() }.add(resourceSettleDuration) |
There was a problem hiding this comment.
question/ Do we need a PriorityQueue()? At the end, all we're looking for is the eventTimestampInNanos of the last stopped resource which can be stored on single property. As long as we can assume that events arrive in order, the same can be achieved without using any data structures:
private var numberOfPendingResources: Int = 0
private var timestampOfLastStoppedResourceNs: Long?
fun resourceWasStarted() {
numberOfPendingResources += 1
}
fun resourceWasStopped(eventTimestampInNanos: Long) {
timestampOfLastStoppedResourceNs = eventTimestampInNanos
numberOfPendingResources -= 1
}There was a problem hiding this comment.
you need a MaxHeap there (in Java that's a PriorityQueue) in order to make sure you have everything ordered as you cannot guarantee the resources are going to be stopped in the same order. So you need to make sure that the last one is the max. In the end you do not need that if you prefer to do the sort when you compute and pick the highest one. For me I prefer this because it is a bit more efficient.
bf70dfa to
08f173d
Compare
6397653 to
1ca4b5f
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## feature/view-loading-times #2392 +/- ##
==============================================================
- Coverage 70.01% 69.87% -0.14%
==============================================================
Files 766 770 +4
Lines 28492 28559 +67
Branches 4782 4793 +11
==============================================================
+ Hits 19948 19955 +7
- Misses 7250 7269 +19
- Partials 1294 1335 +41
|
| private val networkSettledMetricResolver: NetworkSettledMetricResolver = NetworkSettledMetricResolver( | ||
| NetworkSettledRequestIdentificationStrategy.TimeIntervalBasedStrategy(), | ||
| sdkCore.internalLogger | ||
| ) |
There was a problem hiding this comment.
Warning
using default value in internal constructors can lead to mistakes. Since this value depends on the user configuration, it'd be better to remove the default value to ensure the RumViewScope is always initialised with the correct value.
There was a problem hiding this comment.
Based on the RFC we need to provide a strategy with time interval and have default value to 100ms. We can either do this or just provide 2 strategies, one with a constant 100ms interval and the other with a configurable required interval in the constructor: StaticTimeIntervalBasedStrategy, CustomTimeIntervalBasedStrategy but I do think this kind of overkill. WDYT.
| internal sealed class NetworkSettledRequestIdentificationStrategy { | ||
| internal class TimeIntervalBasedStrategy( | ||
| val timeThresholdNs: Long = TimeUnit.MILLISECONDS.toNanos(DEFAULT_TIME_THRESHOLD_MS) | ||
| ) : NetworkSettledRequestIdentificationStrategy() { | ||
| companion object { | ||
| internal const val DEFAULT_TIME_THRESHOLD_MS = 100L | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Caution
Why are we making this strategy a sealed class? I thought from the RFC we wanted to make this a configurable strategy to let customers decide whether or not a request should be considered an "initial request", so having an interface with one provided time based implementation makes more sense.
E.g.:
interface InitialRequestIdentificationStrategy {
fun isInitialrequest(
resourceInfo,
resourceStartTimestamp,
viewStartTimestamp
): Bool
}There was a problem hiding this comment.
This is not the end of it, we can discuss a bit more about this if you want. There will be another strategy which will accept a configurable interface which will be the RequestValidator which is an interface. I did not expose yet this interface because this will be in another task. The reason why I like strategies is that they are exhaustive and also the client will be able to pick from a definite set. One strategy is based only on a time interval so the above definition doesn't match. The client will have to only define the timeInterval and that will be it he doesn't need to implement any interface. The default value is 100ms as this is defined in the RFC. I guess you will see it better once we add the other tasks also.
| init { | ||
| networkSettledMetricResolver.resourceWasStarted( | ||
| NetworkSettledResourceContext(resourceId, eventTime.nanoTime) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Note
Why are we splitting the computation in both the RumViewScope and RumResourceScope. All Resource related event do go through the RumViewScope first (see the RumViewScope.onStartResource() method), so it might be simpler to compute the network settled directly via the RumViewScope. It also might be simpler to compute it in the RumViewScope instead of a NetworkSettledMetricResolver, wdyt?
e69b97b to
c47fe57
Compare
c47fe57 to
476a5f7
Compare
| internal interface RequestValidator { | ||
| fun validate( |
There was a problem hiding this comment.
Note
Nitpick: maybe we should make the naming a bit more explicit about the purpose of this interface (and use consistent name for Resources: Request is not a word we use in RUM), e.g.:
| internal interface RequestValidator { | |
| fun validate( | |
| internal interface InitialResourceIdentifier { | |
| fun isInitialResource( |
476a5f7 to
39a47fc
Compare
39a47fc to
f1e298b
Compare
What does this PR do?
In this PR we are adding the logic for resolving the newly introduced
Time To Network Settledout-of-the-box view loading metric.Start: Begins with RUM ViewScope initialization.
End: Tied to the end of the last resource load that commenced in the initial 100ms window or defined by the given strategy in the configuration. In case the application went into background or a new view started by the time the initial resources finished loading we will not report this metric for that specific view. In general this could be phrased as:
in case the view was stopped before all the resources were loaded we will not report this metric.
gantt title Timeline for Time-to-Network-Settled dateFormat HH:mm:ss section Time-to-Network-Settled View Becomes Visible: des1, 00:00:00, 00:00:00:100 Network request :done, des2, 00:00:01, 00:00:02 Network request :done, des3, 00:00:01, 00:00:04 Network request :done, des4, 00:00:03, 00:00:06 100ms limit: after des4, 00:00:04 Time-to-Network-Settled Interval :active, des5, 00:00:00, 00:00:06Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)