Skip to content

RUM-7191 Implemented the basic logic for time-to-network-settle view metric#2392

Merged
mariusc83 merged 1 commit into
feature/view-loading-timesfrom
mconstantin/rum-7191/implement-basic-functionality-for-tns-metric
Nov 26, 2024
Merged

RUM-7191 Implemented the basic logic for time-to-network-settle view metric#2392
mariusc83 merged 1 commit into
feature/view-loading-timesfrom
mconstantin/rum-7191/implement-basic-functionality-for-tns-metric

Conversation

@mariusc83

@mariusc83 mariusc83 commented Nov 14, 2024

Copy link
Copy Markdown
Member

What does this PR do?

In this PR we are adding the logic for resolving the newly introduced Time To Network Settled out-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:06
Loading

Motivation

What inspired you to submit this pull request?

Additional Notes

Anything else we should know when reviewing?

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@mariusc83 mariusc83 self-assigned this Nov 14, 2024
@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch 2 times, most recently from c759a92 to 90641b0 Compare November 14, 2024 16:35
Comment on lines +19 to +21
private val resourceStartedTimestamps = HashMap<String, HashMap<String, Long>>()
private val resourceStoppedTimestamps = HashMap<String, PriorityQueue<Long>>()
private val viewCreatedTimestampsByBViewId = HashMap<String, Long>()

@ncreated ncreated Nov 15, 2024

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mariusc83 mariusc83 Nov 15, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch 2 times, most recently from bf70dfa to 08f173d Compare November 18, 2024 19:41
@mariusc83
mariusc83 changed the base branch from develop to feature/view-loading-times November 18, 2024 19:41
@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch 3 times, most recently from 6397653 to 1ca4b5f Compare November 18, 2024 21:17
@mariusc83
mariusc83 marked this pull request as ready for review November 18, 2024 21:19
@mariusc83
mariusc83 requested review from a team as code owners November 18, 2024 21:19
@codecov-commenter

codecov-commenter commented Nov 18, 2024

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 69.87%. Comparing base (a67406e) to head (f1e298b).
Report is 2 commits behind head on feature/view-loading-times.

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     
Files with missing lines Coverage Δ
...roid/rum/internal/domain/scope/RumResourceScope.kt 95.12% <100.00%> (-0.91%) ⬇️
.../android/rum/internal/domain/scope/RumViewScope.kt 94.56% <100.00%> (-0.75%) ⬇️
...l/metric/networksettled/InternalResourceContext.kt 100.00% <100.00%> (ø)
...ric/networksettled/NetworkSettledMetricResolver.kt 100.00% <100.00%> (ø)
...ic/networksettled/NetworkSettledResourceContext.kt 100.00% <100.00%> (ø)
...tworksettled/TimeBasedInitialResourceIdentifier.kt 100.00% <100.00%> (ø)

... and 25 files with indirect coverage changes

Comment on lines +63 to +66
private val networkSettledMetricResolver: NetworkSettledMetricResolver = NetworkSettledMetricResolver(
NetworkSettledRequestIdentificationStrategy.TimeIntervalBasedStrategy(),
sdkCore.internalLogger
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +19
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
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +70 to +74
init {
networkSettledMetricResolver.resourceWasStarted(
NetworkSettledResourceContext(resourceId, eventTime.nanoTime)
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch 3 times, most recently from e69b97b to c47fe57 Compare November 22, 2024 19:03
@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch from c47fe57 to 476a5f7 Compare November 22, 2024 20:12
xgouchet
xgouchet previously approved these changes Nov 25, 2024
Comment on lines +12 to +13
internal interface RequestValidator {
fun validate(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.:

Suggested change
internal interface RequestValidator {
fun validate(
internal interface InitialResourceIdentifier {
fun isInitialResource(

@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch from 476a5f7 to 39a47fc Compare November 26, 2024 08:48
@mariusc83
mariusc83 force-pushed the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch from 39a47fc to f1e298b Compare November 26, 2024 10:16
@mariusc83
mariusc83 requested a review from xgouchet November 26, 2024 10:24
@mariusc83
mariusc83 merged commit 39039d1 into feature/view-loading-times Nov 26, 2024
@mariusc83
mariusc83 deleted the mconstantin/rum-7191/implement-basic-functionality-for-tns-metric branch November 26, 2024 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants