Mobile vitals describe how your application feels to use: how long it takes to launch, whether scrolling stays smooth, how long a screen takes to become useful, and what it costs the device. The Atatus iOS SDK collects most of them automatically once RUM is enabled, and attaches them to the view they belong to.
What is collected
| Vital | Default | Configured with |
|---|---|---|
| Application launch time | On | Nothing to configure. reportAppFullyDisplayed() adds the second metric |
| Frame rate | On | vitalsUpdateFrequency |
| Slow frames | On | trackSlowFrames |
| Long tasks | On, over 100 ms | longTaskThreshold |
| CPU and memory | On | vitalsUpdateFrequency |
| Time to network settled | On | networkSettledResourcePredicate |
| Interaction to next view | On | nextViewActionPredicate |
| App hangs | Off | appHangThreshold, covered in Error Tracking |
Application launch time
Launch is measured in two steps, because "launched" means two different things:
- Time to initial display is the time from process start to your first rendered frame. It is collected automatically, with no configuration.
- Time to full display is the time until the screen is actually useful, which only your code knows. Report it when the first screen has its data:
import AtatusRUM
// Call once, when the first screen is populated and interactive
RUMMonitor.shared().reportAppFullyDisplayed()
Only the first call counts, and it works even if you call it before the first frame is reported. A call later than 90 seconds after launch is discarded, as is an initial display later than 60 seconds, because a value that large describes a suspended process rather than a launch.
Each launch is labelled as a cold or warm start. The SDK reports a cold start on a fresh install, after an application or OS update, after a device restart, or when more than a week has passed since the last launch. Launches that iOS performs in the background to prewarm your application are marked as prewarmed, so they do not distort your launch numbers.
Launch timing is measured from process start, so it is only correct if you initialize the SDK in application(_:didFinishLaunchingWithOptions:). Initializing later, for example in the first view controller, silently shortens every launch measurement.
Frame rate and slow frames
The SDK samples the display refresh rate continuously and reports the average and minimum for each view, along with a flag for views that rendered slowly. ProMotion displays are normalized to a 60 Hz ceiling, so a 120 Hz device does not look twice as good as a 60 Hz one.
Slow frames are reported separately and precisely. A frame that arrives at least one frame interval late is recorded with its start and duration, which is what lets you find the exact moment a scroll stuttered:
RUM.enable(
with: RUM.Configuration(
trackSlowFrames: true
)
)
A frame that takes longer than 700 ms is counted as a frozen frame instead. Hitches longer than your app hang threshold are left to hang reporting, so one stall is not reported twice.
Long tasks
A long task is a block of the main thread long enough to delay rendering. The default threshold is 100 ms:
RUM.enable(
with: RUM.Configuration(
longTaskThreshold: 0.2
)
)
Set nil to turn long tasks off. A long task over 700 ms is also counted as a frozen frame.
Long tasks and app hangs measure the same thing at different scales. Long tasks are frequent and cheap to collect, while hangs are rarer, come with a stack trace, and tell you where the block happened. Use long tasks to see how often it happens and hangs to find out why.
CPU and memory
CPU ticks and the memory footprint are sampled on a timer and reported per view as an average and a maximum. The sampling period follows vitalsUpdateFrequency:
| Value | Period |
|---|---|
.frequent |
0.1 seconds |
.average (default) |
0.5 seconds |
.rare |
1 second |
nil |
Turns CPU, memory, and frame rate collection off |
RUM.enable(
with: RUM.Configuration(
vitalsUpdateFrequency: .rare
)
)
Use .rare when you want the shape of the data at the lowest cost, and .frequent only while investigating something specific. Rate metrics such as CPU per second need a view that lasted at least a second, so very short views report counts but no rates.
Time to network settled
This is the time from a screen appearing until the requests that make up its initial load have finished. The SDK decides which requests count as "initial" with a predicate, and by default a request counts if it starts within 100 ms of the view starting.
That default misses screens that begin loading later, for example after refreshing a token or resolving a cache. Widen the window:
RUM.enable(
with: RUM.Configuration(
networkSettledResourcePredicate: TimeBasedTNSResourcePredicate(threshold: 0.5)
)
)
Or decide per request, when only certain endpoints define readiness:
struct FeedResourcesOnly: NetworkSettledResourcePredicate {
func isInitialResource(from params: TNSResourceParams) -> Bool {
params.timeSinceViewStart <= 1.0 && params.url.contains("/api/v1/feed")
}
}
RUM.enable(
with: RUM.Configuration(
networkSettledResourcePredicate: FeedResourcesOnly()
)
)
The predicate runs on a background thread and must return the same answer for the same input every time. This metric cannot be turned off, only redefined.
Interaction to next view
This is the time between a user's last interaction on one screen and the next screen starting, which is how a slow navigation shows up as the user experienced it. By default the SDK anchors the metric to a tap, click, or swipe that happened within three seconds of the new view:
RUM.enable(
with: RUM.Configuration(
nextViewActionPredicate: TimeBasedINVActionPredicate(maxTimeToNextView: 5)
)
)
Raise the limit when navigation legitimately takes longer, for example behind a prefetch, and lower it to avoid blaming an unrelated earlier tap. Setting nil turns the metric off. To exclude specific interactions, implement the predicate:
struct NavigationTapsOnly: NextViewActionPredicate {
func isLastAction(from params: INVActionParams) -> Bool {
guard params.timeToNextView >= 0, params.timeToNextView <= 3 else {
return false
}
switch params.type {
case .tap, .click:
return !params.name.contains("dismiss")
case .scroll, .swipe, .custom:
return false
}
}
}
Record your own timings
A custom timing records how long something in the current view took, measured from when the view started:
RUMMonitor.shared().addTiming(name: "hero_image_loaded")
Use it for the moments that define your screen, such as a first list of results or a chart finishing its render. A timing needs an active view, and names are unique within a view, so a repeated name overwrites the earlier value. Characters outside letters, digits, _, ., @, $, and - are replaced with an underscore.
Platform support
| Platform | Notes |
|---|---|
| iOS, iPadOS | Everything on this page is collected |
| tvOS | Same as iOS, except that actions are press based |
Next steps
- Report hangs and crashes with Error Tracking.
- See which requests a slow screen is waiting on with Network Tracking.
- Review the rest of the options in Configuration Options.
+1-415-800-4104