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:
copy
icon/buttons/copy
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.

Warning:

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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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.

Note:

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
copy
icon/buttons/copy
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:

copy
icon/buttons/copy
RUM.enable(
    with: RUM.Configuration(
        networkSettledResourcePredicate: TimeBasedTNSResourcePredicate(threshold: 0.5)
    )
)

Or decide per request, when only certain endpoints define readiness:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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:

copy
icon/buttons/copy
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