Skip to main content

10 posts tagged with "App Hang"

View All Tags

How Appxiom MCP Helps iOS Developers Debug and Fix Bugs Faster

Published: · 14 min read
Don Peter
Cofounder and CTO, Appxiom

The iOS Debugging Crisis: The Illusion of "99.9% Crash-Free"​

Every experienced iOS developer knows the sinking feeling of opening a bug report from production:

  • "The app froze when I tapped Pay with Apple Pay."
  • "Everything became unresponsive on my iPhone 15 Pro after scrolling for 20 seconds."
  • "The app just randomly vanished while I was reviewing my cart."

You open your APM dashboard: Firebase Crashlytics, Sentry, or Datadog. The dashboard proudly displays a 99.9% Crash-Free Users vanity metric. There are no fatal crash logs. No EXC_BAD_ACCESS, no SIGSEGV, no unhandled Swift panics.

Yet, your App Store rating is dropping, and product analytics show a 22% drop-off in the checkout funnel.

The Traditional iOS Debugging Tax​

Step / MetricDiagnostic StageTraditional iOS Debugging Experience
1. Bug DetectionVague Support TicketAmbiguous user reports or negative App Store reviews
2. APM VisibilityCrashlytics False Security99.9% Crash-Free Users (zero fatal crashes recorded in APM tools)
3. Local Reproduction"Works on My Simulator" Trap2–4 hours guessing gestures, thermal states & network latency
4. Diagnostic IngestionRaw Log ExtractionDumping raw console / IPS logs (token bloat, unsymbolicated hex addresses & AI hallucinations)
5. Manual ProfilingInstruments InvestigationSpinning up Xcode Instruments manually (hand-profiling Time Profiler & Allocations)
6. Resolution TimeTotal MTTR6 to 8 Hours per Regression

The Anatomy of Silent iOS Failures​

Traditional crash reporters were architected a decade ago for an era when bugs meant fatal process crashes. But modern iOS applications rarely die from catastrophic memory corruption. Instead, they suffer from silent, conversion-killing runtime friction:

  1. iOS App Hangs (>250ms Main-Thread Freezes): Apple's MetricKit defines an App Hang as any period where the main run loop (CFRunLoop) is blocked from processing events for longer than 250 milliseconds. The app does not crash: it simply becomes a brick. Users tap the screen three times, assume the app is broken, force-quit, and leave.
  2. Watchdog Terminations (0x8badf00d): If an operation blocks the main thread for too long during launch (5 seconds) or transitions (10–20 seconds), Apple's OS Watchdog kills the process with exception code 0x8badf00d ("ate bad food"). Traditional tools frequently classify these as generic terminations without an actionable call stack.
  3. Swift 6 @MainActor Contention & Deadlocks: With Swift 6 strict concurrency, operations mistakenly isolated to the @MainActor (synchronous Keychain reads, disk I/O, cryptographic hashing, CoreData/SwiftData faulting) stall the UI thread while awaiting system locks.
  4. Memory Retention & Jetsam Terminations (0xdead10cc): Escaping closures missing [weak self], retained SwiftUI NavigationStack coordinators, or unbounded image caches push memory past the device's high-water mark, causing the iOS kernel (jetsam) to silently terminate the process.
  5. Silent State Deadlocks in SwiftUI: Swallowed try? expressions, unhandled network timeouts, or unhandled 4xx/5xx responses that leave views stranded indefinitely on blank screens or spinning progress views.

Why Raw iOS Logs Break AI Coding Assistants​

When developers ask AI coding assistants (Cursor, Claude Code, Antigravity) to help debug, they typically copy-paste messy Xcode console logs, raw MetricKit JSON, or unsymbolicated .ips crash files.

This immediately degrades the AI agent's performance:

  • Token Exhaustion: Raw logs contain thousands of lines of noisy system prints (nw_connection, CoreAnimation transaction commits, metal compiler notices).
  • Demangling & Symbolication Blindness: Raw memory addresses (0x00000001048b2a34) provide zero semantic meaning to an LLM without DWARF symbolication.
  • Context Degradation & Hallucinations: Because the AI agent cannot see the user's preceding journey or thread state, it hallucinates circular, speculative fixes (e.g., advising you to slap DispatchQueue.main.async everywhere).

Comprehensive Detection of iOS Issues​

Appxiom eliminates the iOS crash reporting blindspot. Traditional APMs only listen for catastrophic process terminations, missing the silent performance and user friction bugs that drive real churn.

Through a lightweight, zero-overhead Swift SDK, Appxiom continuously monitors the iOS runtime and automatically tracks 10 comprehensive issue types across responsiveness, memory, networking, and critical feature stability, with zero manual breadcrumb logging required:

  1. iOS App Hangs: Captures unresponsive main-thread behavior (>250ms) where CFRunLoop stalls, freezing the user interface without crashing.
  2. Cold Start Delays: Pinpoints startup bottlenecks during app initialization and launch sequences to optimize cold launch times.
  3. Screen Load Delays: Detects slow view transitions, layout stalls, and heavy rendering passes that leave users staring at blank screens.
  4. iOS Crashes: Automatically captures unhandled exceptions, EXC_BAD_ACCESS, fatal memory corruptions, and panics with full symbolication.
  5. Memory Leaks: Detects retain cycles, unreleased closures, and leaking view controllers during development and in production.
  6. Abnormal Memory Usage: Monitors high memory spikes and runaway heap consumption before the OS triggers memory warnings or termination.
  7. HTTP API Failures: Detects network request failures, timeouts, 4xx/5xx HTTP errors, and dropped network calls impacting user journeys.
  8. Crucial Feature Failures: Tracks core business feature breakdowns (such as failed payment flows or broken checkout) to prevent user drop-offs.
  9. Function Failures: Identifies failures in specific underlying functions and critical methods that break user-facing features.
  10. Custom Issues & Failures: Captures domain-specific exceptions, assertions, and custom business failure patterns tailored to your app.

Automatic iOS App Hang Detection: The Ground Truth​

Unlike MetricKit, which aggregates hangs into delayed 24-hour diagnostic batches, Appxiom monitors the CFRunLoop in real time.

The moment a main-thread block exceeds 250ms, Appxiom:

  1. Samples the exact call stack of com.apple.main-thread.
  2. Demangles all Swift symbols down to the exact source file and line number.
  3. Records the wall-clock stall duration.
  4. Captures the active device state: device model (e.g., iPhone 15 Pro), thermal state (ProcessInfo.ThermalState), battery level, and memory pressure.

How Appxiom MCP Works: Structured Ground Truth for iOS AI Agents​

Detecting these critical iOS issues in production is half the battle. The true leap in developer velocity happens when this telemetry is piped directly into your AI coding agent via the Model Context Protocol (MCP).

MCP is the open JSON-RPC standard that enables AI models in your editor (Cursor, Claude Code, Antigravity) to query external development tools directly.

Instead of switching between Xcode, Sentry, App Store Connect, and terminal logs, Appxiom MCP exposes your iOS production runtime as structured, machine-readable JSON tools inside your editor.

Raw Xcode Logs vs. Appxiom Structured MCP Data​

Diagnostic PropertyTraditional Raw iOS LogsAppxiom MCP Structured Telemetry
Data FormatUnstructured console prints, hex addressesClean, typed JSON schema
Token Overhead4,000–8,000 tokens of noisy system output~350–500 tokens of pure diagnostic signal
User JourneyCompletely invisibleActivity Trail: Chronological array of user actions/events
Stack TraceUnsymbolicated machine offsetsDemangled Swift 6 symbols with file and line numbers
Business ImpactUnordered error countGoal Friction Impact (GFI): Funnel drop-off and revenue loss
AI Hallucination RateHigh (speculative guessing)Near zero (deterministic runtime proof)

The 4 Structured Pillars Appxiom MCP Passes to Your Agent​

When your AI coding assistant queries Appxiom MCP, it receives four structured inputs:

  1. The SwiftUI / UIKit Activity Trail: A deterministic flight-recorder array of user taps, screen transitions, view presentations, and network calls leading to the stall: CartView ➔ AddressSelectionView ➔ PaymentMethodView ➔ Button("Confirm Apple Pay").tap
  2. Normalized Swift Thread Diagnostics: Demangled call trees showing the blocked thread (com.apple.main-thread), lock contention, and the exact offending line in your Swift file.
  3. Hardware & Environment Context: Device model (iPhone 16 Pro), thermal condition (ProcessInfo.thermalState = .serious), low-power mode status, and available RAM.
  4. Goal Friction Impact (GFI): A quantified metric linking the technical glitch directly to business impact (e.g., "This App Hang causes a 24.2% drop-off in the checkout funnel with an estimated $14,200/hr loss").

The 4-Step Accelerated iOS Debugging Workflow​

With Appxiom MCP integrated into your editor, the iOS debugging loop compresses from an entire afternoon to under 15 minutes:

StepWorkflowWhat HappensTime
1Intelligent Triage via MCPAgent queries Appxiom MCP and identifies the highest-friction regressions by GFI.30 seconds
2Ingest the SwiftUI Activity TrailAgent loads exact user actions and the view stack, eliminating guesswork.1 minute
3Precision Root-Cause IsolationAgent inspects the demangled Swift stack trace and pinpoints the exact line causing the issue.2 minutes
4Automated Swift 6 TDD Fix & Local VerificationAgent writes tests, refactors using Swift 6 concurrency patterns, and runs the test suite.5–8 minutes
ResultVerified Production Bug FixThe issue is fixed and locally verified.Under 15 minutes

Real-World iOS Bug Walkthroughs: The Workflow in Action​

Here are three real-world iOS engineering scenarios demonstrating how Appxiom MCP transforms complex debugging into immediate, deterministic fixes.

Walkthrough 1: The Invisible iOS Checkout Hang (Synchronous Keychain on @MainActor)​

The Symptom​

Users on iOS 18 tap "Pay with Apple Pay" during checkout, and the screen freezes for nearly two seconds. No crash report appears in Crashlytics. However, analytics indicate a 24.2% drop-off at this step.

How Appxiom MCP Surfaced It​

Appxiom automatically detected an iOS App Hang of 1,840ms on com.apple.main-thread.

Inside Cursor, the developer prompts:

"Query Appxiom MCP for the highest-friction checkout drop-offs on iOS."

The AI agent receives Appxiom's structured diagnostic payload:

  • Anomaly: ios_app_hang
  • Duration: 1,840ms
  • Thread: com.apple.main-thread
  • Activity Trail: CartView ➔ CheckoutView ➔ PaymentSelectionView ➔ Button.tap("Apple Pay")
  • Demangled Stack:
    0 libsystem_kernel.dylib         mach_msg_trap + 8
    1 Security SecItemCopyMatching + 148
    2 ShopAppCore KeychainWrapper.readToken(key:) (KeychainWrapper.swift:42)
    3 CheckoutFeature CheckoutViewModel.processApplePay() (CheckoutViewModel.swift:188)
    4 SwiftUI Button.action.callAsFunction() + 32

The Root Cause​

SecItemCopyMatching is a synchronous C-function that communicates with the securityd daemon over Mach ports. Under biometric authentication or system load, this call blocks the calling thread. Because CheckoutViewModel was marked @MainActor, it blocked the main thread for 1.84 seconds, freezing the UI.

The Swift 6 Fix​

The AI agent immediately understands the issue: the Keychain call must be isolated to a background actor.

// BEFORE (Buggy Implementation - Freezing @MainActor)
@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var isProcessing: Bool = false

func processApplePay() {
isProcessing = true
// Synchronous Mach message to securityd blocks MainActor for 1,840ms
guard let token = KeychainWrapper.shared.readToken(key: "auth_token") else {
return
}
executePayment(with: token)
}
}

// AFTER (Appxiom MCP-Guided Fix - Actor-Isolated Background Concurrency)
actor KeychainAsyncService {
static let shared = KeychainAsyncService()

/// Reads Keychain items off the MainActor on the cooperative background thread pool
func readToken(key: String) -> String? {
return KeychainWrapper.shared.readToken(key: key)
}
}

@MainActor
final class CheckoutViewModel: ObservableObject {
@Published var isProcessing: Bool = false

func processApplePay() async {
isProcessing = true
// Non-blocking asynchronous suspension point
guard let token = await KeychainAsyncService.shared.readToken(key: "auth_token") else {
isProcessing = false
return
}
await executePayment(with: token)
isProcessing = false
}
}

The agent writes a unit test using the modern Swift Testing framework asserting that readToken runs off the main thread:

// Tests/CheckoutTests/KeychainAsyncTests.swift
import Testing
@testable import ShopAppCore

@Suite("Keychain Asynchronous Security Tests")
struct KeychainSecurityTests {
@Test("Verify Keychain query does not block MainActor")
func testKeychainQueryOffMainActor() async throws {
let isMain = await Task.detached {
let _ = await KeychainAsyncService.shared.readToken(key: "auth_token")
return Thread.isMainThread
}.value

#expect(!isMain, "Keychain read must execute off the main thread")
}
}

Resolution Time: 11 minutes.

Walkthrough 2: SwiftUI NavigationStack Retention & Memory Spike (Jetsam Kill)​

The Symptom​

Users browsing high-resolution product galleries in an e-commerce app experienced random app disappearances after navigating between 8–10 products. Crashlytics logged generic OS terminations with zero stack traces.

How Appxiom MCP Surfaced It​

Appxiom detected an iOS Memory Leak / Jetsam Warning:

  • Heap memory jumped from 112 MB to 1.42 GB in under 90 seconds.
  • OS emitted UIApplication.didReceiveMemoryWarningNotification three times before the kernel terminated the process.
  • Activity Trail: The user repeatedly pushed and popped ProductDetailView inside a NavigationStack.

The Root Cause​

The structured memory graph diagnostics provided by Appxiom MCP pointed to an escaping coordinator closure in ProductDetailView:

// BEFORE (Buggy Implementation - Strong Reference Retain Cycle)
struct ProductDetailView: View {
@StateObject var viewModel: ProductViewModel
let onAddToCart: (Product) -> Void

var body: some View {
VStack {
ImageCarousel(images: viewModel.highResImages)
Button("Add to Cart") {
// Escaping closure captures `viewModel` strongly,
// preventing NavigationStack view teardown
onAddToCart(viewModel.product)
}
}
}
}

Every time a user popped the view, the ProductViewModel and its decoded image buffers (highResImages, ~120MB per screen) remained allocated in heap memory because the navigation coordinator closure held a strong reference cycle.

The Swift Fix​

The AI agent:

  1. Injected an explicit [weak viewModel] capture list into the closure.
  2. Refactored the image pipeline to downsample thumbnails using ImageIO (CGImageSourceCreateThumbnailAtIndex) rather than decoding raw 4K buffers directly into memory.
  3. Verified via automated tests that instances of ProductViewModel successfully deallocate on onDisappear.

Resolution Time: 13 minutes.

Walkthrough 3: SwiftData / CoreData Main-Thread Faulting (ProMotion Frame Drop)​

The Symptom​

Users on 120Hz ProMotion displays (iPhone 15 Pro, iPhone 16 Pro) experienced severe UI stuttering and hitching when scrolling through an offline order history list containing 500+ items.

How Appxiom MCP Surfaced It​

Appxiom automatically detected an iOS Main-Thread Hitch & Sub-Second App Hang:

  • Hitch duration: 680ms (causing 41 dropped frames at 120fps).
  • Offending thread: com.apple.main-thread.
  • Stack trace pinpointed: NSManagedObject.value(forKey:) triggering synchronous relationship faulting during SwiftUI list cell rendering.

The SwiftData / CoreData Fix​

The agent recognized that accessing faulted relationships (order.items) inside SwiftUI's ForEach was executing synchronous SQLite reads on the main thread.

The agent refactored the fetch logic to use prefetching and background DTO projection:

// BEFORE (Buggy Implementation - Synchronous Faulting on MainActor)
struct OrderRowView: View {
let order: Order // Managed object

var body: some View {
HStack {
Text(order.orderNumber)
// Triggering synchronous relationship faulting on the main thread for each cell
Text("\(order.items.count) items")
}
}
}

// AFTER (Appxiom MCP-Guided Fix - Background DTO Projection)
struct OrderSummaryDTO: Sendable, Identifiable {
let id: UUID
let orderNumber: String
let itemCount: Int
}

actor OrderDataService {
func fetchOrderSummaries(context: ModelContext) throws -> [OrderSummaryDTO] {
var descriptor = FetchDescriptor<Order>(
sortBy: [SortDescriptor(\.timestamp, order: .reverse)]
)
descriptor.relationshipKeyPathsForPrefetching = [\.items]

let orders = try context.fetch(descriptor)
return orders.map { OrderSummaryDTO(id: $0.id, orderNumber: $0.orderNumber, itemCount: $0.items.count) }
}
}

The hitch rate dropped to 0%, restoring buttery 120fps ProMotion scrolling.

Resolution Time: 10 minutes.

The Quantified MTTR Advantage for iOS Teams​

When iOS runtime telemetry is accessible as structured data directly inside your AI coding assistant, debugging cycles are radically transformed:

Stage of ResolutionTraditional iOS WorkflowAppxiom MCP + AI AgentSpeedup
1. Triage & Prioritization45–60 mins
• Sifting through noisy Crashlytics lists
• Deciphering vague support tickets
1 min
• Query MCP sorted by Goal Friction Impact
• Identifies the exact revenue-impacting flow
98% faster
2. Issue Reproduction2–4 hours
• Rebuilding on physical devices
• Guessing user taps and thermal states
2 mins
• Agent parses structured Activity Trail
• Exact view and gesture stack loaded
98% faster
3. Root-Cause Analysis1–2 hours
• Profiling manually in Instruments
• Symbolicating hex stack offsets
2 mins
• Demangled Swift stack points directly to the line
• Thread contention & stall duration exposed
96% faster
4. Fix & Verification60 mins
• Manual code patches
• Hoping concurrency bugs don't regress
5–8 mins
• Agent writes Swift Testing reproduction test
• Implements Swift 6 Actor fix & runs tests green
88% faster
Total MTTR5 to 8 Hours~10 to 15 Minutes~96% faster

Developer Quick-Start: Connecting Appxiom MCP to Your iOS Environment​

Connecting Appxiom MCP to your AI coding agent (Cursor, Claude Code, Antigravity) takes under two minutes.

Connect Appxiom MCP to Your AI Agent​

Connecting is effortless: as long as you are logged into your Appxiom dashboard, adding the MCP server to your AI agent requires just two simple steps without managing complicated authentication tokens:

  1. Get the MCP URL: Copy the Appxiom MCP server URL provided in the Appxiom Documentation.
  2. Add to Your AI Agent: Paste the MCP URL directly into your coding agent's MCP settings (such as in Cursor, Claude Code, or Antigravity).

Because your active Appxiom dashboard session authenticates access seamlessly, your AI assistant is instantly ready to query live production telemetry, user journeys, and thread diagnostics.

Conclusion: Zero-Guesswork iOS Engineering​

Debugging on iOS shouldn't mean guessing user steps, deciphering hexadecimal stack dumps, or waiting for delayed MetricKit reports.

By uniting comprehensive detection of critical iOS issues with the structured intelligence of Model Context Protocol (MCP), Appxiom bridges the gap between Apple's runtime reality and your editor.

Your AI coding assistants stop hallucinating, your team stops wasting 70% of its bandwidth on manual reproduction, and silent App Hangs are diagnosed, fixed, and verified in minutes.

Silent Failures, ANRs, and App Hangs: How to Detect and Fix the Bugs That Never Crash

Published: · 15 min read
Don Peter
Cofounder and CTO, Appxiom

Your crash reporting dashboard displays a pristine 99.9% crash-free session rate.

Your engineering team celebrates a successful deployment. The release metrics look flawless. But on the App Store and Google Play, your reviews tell a completely different story:

"The app freezes every time I tap the checkout button. Completely unusable."
"It just spins forever on the login screen. Had to force close and uninstall."
"Buttons become unresponsive after opening my transaction history."

Meanwhile, your product manager pulls up analytics: checkout funnel completion has plummeted by 22%, and customer churn is spiking. Yet your error monitor reports zero fatal crashes.

How can an application be catastrophic for users while registering as 100% healthy on your APM dashboard?

Welcome to the blind spot of modern software monitoring: Silent Failures, Application Not Responding (ANR) events, and iOS App Hangs.

The Dangerous Illusion of the Crash-Free Rate​

For more than a decade, mobile engineering teams have used Crash-Free Session Rate (CFSR) as their primary release quality benchmark.

The flaw with CFSR is structural: it measures process survivability, not user experience.

When a mobile app freezes, the process is still running. When an API call fails and leaves a progress indicator spinning indefinitely, no unhandled exception is thrown. When a user force-closes the app in frustration, the operating system registers a user termination, not a crash.

To your crash logger, that session was a victory. To your user, it was an infuriating failure.

Deconstructing the Three Horsemen of Silent Decay​

To eliminate silent failures, engineering teams must understand the three distinct technical mechanisms that cause them:

1. Android ANRs (Application Not Responding)​

On Android, the operating system monitors UI responsiveness via the system server. If your application cannot process input events or lifecycle callbacks within designated timeframes, Android displays the dreaded Application Not Responding (ANR) dialog:

app is not responding

Close app
Wait

What Triggers an ANR?​

  • Input Dispatching Timeout: The app fails to respond to an input event (e.g., key press or screen touch) within 5 seconds.
  • BroadcastReceiver Timeout: A BroadcastReceiver fails to finish executing within 10 seconds in foreground mode.
  • Service Execution Timeout: A Service executed by the foreground app fails to complete its lifecycle tasks within 20 seconds.

The Business Penalty: Google Play Vitals​

Google Play monitors your app's User-Perceived ANR Rate (ANRs occurring when your app is in the foreground). If your ANR rate exceeds Google's bad behavior threshold of 0.47% overall (or 8% on specific phone models):

  • Google demotes your app in Play Store search rankings.
  • Your app is disqualified from store featuring and promotional carousels.
  • Google Play displays a warning directly on your store listing: "Recent data shows that this app may stop working on your device."

2. iOS App Hangs​

iOS does not display an ANR dialog. Instead, when the main run loop is blocked, the interface simply stops accepting touch input. Tapping buttons produces no ripple effect, scrolling locks up, and animations stutter to a halt.

What Triggers an App Hang?​

The iOS main thread operates as an event run loop (CFRunLoop). When an operation performs synchronous disk I/O, heavy computation, or waits on a dispatch semaphore on the main thread, the run loop cannot process new touch events.

  • Micro-Hangs (250ms – 500ms): Perceived by users as "jank," dropped frames, or sluggishness.
  • Severe Hangs (> 2,000ms): Users perceive the app as completely frozen. Over 70% of users will force-quit an app if an interaction takes longer than 3 seconds without visual feedback.

Apple Xcode Organizer Telemetry​

Apple tracks your Hang Rate (measured as milliseconds of hang time per hour of active use). High hang rates directly suppress your visibility in App Store search results and elevate user churn.

3. Silent Functional Failures & Swallowed Exceptions​

Unlike ANRs and App Hangs, silent functional failures often involve zero thread contention. Instead, they are caused by defensive programming gone wrong:

// The Classic "Silent Killer" Pattern:
try {
val response = paymentApi.confirmPurchase(orderId)
updateUiWithReceipt(response)
} catch (e: Exception) {
// Exception caught and logged to console or an unmonitored analytics tag
Log.e("Payment", "Purchase failed", e)
// The spinner continues to animate, or the screen never transitions!
}

The network request times out, or the backend returns a 503 Service Unavailable. Because the exception was caught, no crash occurred. But the user is left looking at an unresponsive checkout button. They tap it three more times, get no feedback, and leave your app forever.

Why Traditional APMs Fail to Diagnose ANRs and Hangs​

Most engineering teams rely on crash loggers to debug ANRs. When an ANR occurs, these tools capture a point-in-time thread dump. But in production, these thread dumps are notoriously misleading:

1. The "Innocent Bystander" Stack Trace​

When an operating system triggers an ANR dump at second 5, the thread dump records whatever code happens to be executing at that exact millisecond. Frequently, this is an innocent operating system wait call:

"main" prio=5 tid=1 Native
#00 pc 000000000004b344 /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
#01 pc 00000000001ae4f8 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks)

The stack trace tells you the main thread is waiting for a lock. But which thread holds the lock? And what action caused the lock to be acquired in the first place? The thread dump cannot answer either question.

2. Lack of Preceding User Interaction Context​

Did the user tap a filter button? Did they background the app and return? Did their network connection drop from Wi-Fi to cellular right before the freeze? Standard thread dumps provide zero historical sequence.

3. No Correlation to Business Impact​

If an ANR occurs during app startup on an obsolete Android 8 device, it is an annoyance. If an App Hang occurs every time a high-value customer taps "Complete Purchase" on iOS 18, it is a business catastrophe. Traditional tools treat both events as isolated technical entries.

How Appxiom Solves Silent Failures: Continuous Forensics​

Appxiom tackles silent failures by bridging the gap between technical thread execution and user experience:

1. Proactive Main-Thread Watchdogs​

Appxiom continuously monitors UI thread responsiveness and runloop latency. If a task stalls the UI thread for longer than your defined threshold, Appxiom captures the event before the user force-closes the application.

2. The Activity Trail: Reconstructing the Crime Scene​

Instead of an isolated thread dump, Appxiom attaches the complete Activity Trail to every ANR and App Hang report:

APPXIOM ANR REPORT

Issue: ANR Detected (Main thread blocked for 5,240ms)
Version: v3.4.1 (Build 92)  |  Device: Samsung Galaxy S23  |  OS: Android 14

TIME

TYPE

DETAILS

FREE MEM

14:10:02.115LIFECYCLEOrdersActivity:onResume52.1%
14:10:03.450USER ACTIONClicked "Sync Offline Orders"51.8%
14:10:03.480CUSTOMlocal_db_query_started51.8%
14:10:03.490HTTPPOST /api/v2/orders/sync → 200 OK (310ms)51.2%
14:10:03.810CUSTOMparsing_5000_json_items49.5%
14:10:08.730ISSUE

ANR: Main thread stalled during JSON parsing for 5,240ms

48.9%

What this tells the engineer:​

In 15 seconds, the developer sees what happened:

  1. The user tapped 'Sync Offline Orders'.
  2. The network request succeeded quickly (200 OK in 310ms).
  3. The response contained 5,000 JSON items.
  4. The JSON parsing and database insertion were executed synchronously on the main thread instead of offloaded to a background coroutine or thread pool.
  5. The UI froze for 5.2 seconds, triggering the ANR.

3. Goal Friction Impact (GFI)​

Through Goal Friction Impact, Appxiom correlates silent ANRs and hangs directly to user drop-offs in critical funnels (such as checkout, signup, or subscription).

If a silent freeze occurs in a settings menu, its GFI score is low (e.g., 1.2/10). If an App Hang occurs during credit card validation, its GFI spikes to 9.6/10, immediately alerting your team to a revenue-critical blocker.

4. Appxiom Quality Score (QS)​

Unlike binary crash-free rates, the Appxiom Quality Score evaluates version health on a unified 0–10 scale that penalizes ANRs, App Hangs, and slow screen loads alongside fatal crashes.

Technical Best Practices: Preventing Main-Thread Freezes​

Eliminating ANRs and App Hangs requires architectural discipline across your codebase. Here are the core patterns to enforce:

Android: Moving Work Off the Main Thread​

1. Enforce Background Dispatchers for I/O​

Never perform disk reads, SharedPreferences writes, Room queries, or network operations on Dispatchers.Main:

// BAD: Runs on Main Thread, causes ANR under heavy DB load
fun loadUserTransactions() {
val transactions = database.transactionDao().getAll() // BLOCKS UI THREAD
updateUi(transactions)
}

// GOOD: Offloaded to Dispatchers.IO with Kotlin Coroutines
fun loadUserTransactions() = viewModelScope.launch {
val transactions = withContext(Dispatchers.IO) {
database.transactionDao().getAll()
}
updateUi(transactions)
}

2. Enable StrictMode in Debug Builds​

Catch disk and network violations during development before they ever reach production:

if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}

iOS: Eliminating Runloop Blocking in Swift​

1. Leverage Swift 6 Concurrency & Actor Isolation​

Ensure heavy computations are decoupled from the @MainActor:

// BAD: Synchronous heavy parsing on the main actor
@MainActor
func processOrderHistory(data: Data) {
let decoder = JSONDecoder()
// Heavy JSON parsing locks the main runloop
let orders = try? decoder.decode([Order].self, from: data)
self.displayOrders(orders)
}

// GOOD: Background task processing
@MainActor
func processOrderHistory(data: Data) async {
let orders = await Task.detached(priority: .userInitiated) {
let decoder = JSONDecoder()
return try? decoder.decode([Order].self, from: data)
}.value

self.displayOrders(orders)
}

2. Avoid Synchronous Semaphore Waits on the Main Queue​

Never call dispatch_semaphore_wait or DispatchGroup.wait() on the main thread while waiting for a background completion handler. If the background thread requires main-thread access to complete, your app enters an immediate deadlock.

Flutter: Isolates for Heavy Computation​

In Flutter, the Dart engine runs on a single event loop. If you perform heavy operations on that loop, you freeze the UI across both Android and iOS simultaneously:

// BAD: Blocks Flutter root isolate
void parseLargeJson(String rawJson) {
final data = jsonDecode(rawJson); // Drops frames or triggers ANR
setState(() => items = data);
}

// GOOD: Offload to background Isolate using compute()
Future<void> parseLargeJson(String rawJson) async {
final data = await compute(jsonDecode, rawJson);
setState(() => items = data);
}

Comparison: Fatal Crash Monitoring vs. Appxiom Observability​

CapabilityTraditional Crash Loggers (Crashlytics, Bugsnag)Appxiom Performance & Error Platform
Fatal Exception Tracking✅ Yes✅ Yes
Android ANR Detection⚠️ Post-mortem thread dumps only✅ Real-time watchdog + Activity Trail
iOS App Hang Detection❌ Limited or omitted✅ Micro-hang & severe hang tracking
Silent API & State Failures❌ Omitted (process stays alive)✅ Automatic 4xx/5xx & flow block detection
User Interaction Footsteps❌ Generic breadcrumbs✅ Auto-tracked taps, gestures & lifecycles
Business Impact Metric❌ Occurrence count only✅ Goal Friction Impact (0–10 GFI Score)
Holistic Version Quality❌ Binary Crash-Free Rate✅ Appxiom Quality Score (0–10 QS)

The End of Invisible Bugs​

A crash that affects 10 users in an obscure background service is easy to spot on your dashboard.

An App Hang that freezes 5,000 customers during checkout is far more devastating—yet traditional monitoring platforms let it slip by in complete silence.

Stop relying on vanity metrics that disguise broken user experiences as healthy releases.

With Appxiom, you detect the failures that matter most:

  • Pinpoint the exact line of code causing your ANRs and App Hangs.
  • Retrace the user footsteps that led to the freeze.
  • Protect your App Store rankings and Google Play Vitals.
  • Fix release regressions before they cost you revenue.

Stop Letting Silent Failures Drain Your Revenue​

See how Appxiom gives mobile engineering teams the clarity they need to eliminate ANRs, App Hangs, and invisible regressions.

Start Your Free Appxiom Trial Today or explore our types of bugs detected in mobile and web apps.

Stop Guessing 'How to Reproduce': Debugging Release Regressions with Appxiom's Activity Trail

Published: · 17 min read
Andrea Sunny
Marketing Associate, Appxiom

You just shipped version v4.2.0 of your mobile application. Every unit test passed in CI, staging looked rock-solid, and the release train left the station on schedule.

Twenty-four hours later, the Slack notifications start pouring in.

Customer support is escalating tickets. App Store reviews are dropping from 4.8 to 3.2 stars. Product managers are asking why checkout conversion dropped by 18%. And on Jira, you are assigned a ticket titled:

"App crashes or freezes when user tries to place an order after applying coupon."
Steps to Reproduce: "Unknown. Just tap around in the cart."

You pull up your error tracker - Firebase Crashlytics, Sentry, or Bugsnag. You find the stack trace:

Fatal Exception: java.lang.IllegalStateException: 
Can not perform this action after onSaveInstanceState
at androidx.fragment.app.FragmentManager.checkStateLoss(FragmentManager.java:1844)
at androidx.fragment.app.FragmentManager.enqueueAction(FragmentManager.java:1884)
at androidx.fragment.app.BackStackRecord.commitInternal(BackStackRecord.java:329)
at com.example.store.checkout.CheckoutFragment.showOrderConfirmation(CheckoutFragment.kt:342)

You open the codebase. You launch the emulator on your workstation. You add an item to the cart, apply a coupon, tap checkout, and... it works completely fine.

You test it on five different test devices. It works fine on every single one.

You reply to the ticket: "Unable to reproduce locally. Need more info." QA replies: "We saw it twice on staging, but can't replicate it consistently." Support asks the customer for a screen recording. The customer ignores the email and uninstalls the app.

Welcome to the most demoralizing, soul-crushing ritual in modern software engineering: debugging production release regressions without knowing what the user actually did.

Why Stack Traces Fail: The Point-of-Impact Fallacy in Production Regressions​

For the past fifteen years, engineering teams have relied on stack traces as the primary source of truth for software defects. But mobile applications have evolved into complex, asynchronous, multi-threaded state machines.

When an issue occurs in production, a stack trace only tells you where the process failed (the point of impact). It tells you virtually nothing about the sequence of events that created the corrupted state (the crime scene).

Traditional Crash ReportingVisibility / Details
Point-of-Impact Stack TraceLine 342 in CheckoutFragment
"What broke?"IllegalStateException
"What was the user doing 5 seconds before?"❓ Unknown
"Which buttons were tapped?"❓ Unknown
"Did the network drop or switch?"❓ Unknown
"Was the app backgrounded and restored?"❓ Unknown

Consider the typical root causes of production release regressions that make them so notoriously hard to reproduce:

1. Asynchronous Race Conditions & Network Latency Spikes​

The user taps a button while an HTTP request is still in flight. A fast 5G connection in your office resolves the promise in 80 milliseconds. But a user on a commuter train with fluctuating 4G encounters a 1,800-millisecond latency spike. During that delay, they tap the button a second time, background the app to check an SMS verification code, and return. The response arrives when the UI fragment is already detached. The crash happens in an Android lifecycle callback, but the root cause was the unhandled button re-tap during a cellular latency spike.

2. Silent Failures, ANRs, and UI Freezes (App Hangs)​

A staggering percentage of release regressions never generate a fatal exception.

  • Android ANRs (Application Not Responding): The main thread stalls on disk I/O, lock contention, or heavy computation for more than 5 seconds.
  • iOS App Hangs: The main run loop blocks, rendering touch targets completely unresponsive.
  • Network Dead-Ends: An API responds with a 404 or 500 error that was swallowed in a generic try/catch block, leaving the loading spinner spinning indefinitely.

Because the operating system didn't terminate the process with a fatal signal, traditional crash loggers record either nothing or an unhelpful system thread dump that points to an idle OS loop.

3. The Breadcrumb Noise Problem​

Some APM tools attempt to capture "breadcrumbs," but they typically suffer from two extremes:

  • The Firehose: Millions of unindexed log lines that pollute dashboards, cost a fortune in ingestion fees, and drown engineers in useless console chatter.
  • The Desert: Generic system events like Activity Resumed without details on button clicks, touch targets, memory pressure, or offline state changes.

When a regression strikes, engineers don't need a million lines of unstructured text. They need a clean, chronological flight recorder that reconstructs the exact user footsteps prior to the failure.

What is Appxiom's Activity Trail? The Black Box Flight Recorder for Apps​

Appxiom was engineered to eliminate the "Cannot Reproduce" ticket entirely.

At the heart of Appxiom's diagnostic engine is the Activity Trail: a chronologically ordered, high-fidelity timeline of events, user interactions, lifecycle shifts, and network state changes captured immediately prior to any issue occurrence.

[!NOTE] What is an Activity Trail?
An Activity Trail is a chronologically ordered, microsecond-accurate timeline of user interactions (taps, clicks, gestures), screen and component lifecycle transitions, network connectivity changes, and custom developer markers captured immediately prior to an issue occurrence. It enables engineers to retrace exact user steps and reproduce release regressions without guesswork or screen recordings.

When an issue occurs - whether it is an uncaught exception, a memory leak, an ANR, an iOS App Hang, or an API failure - Appxiom packages the stack trace together with the preceding Activity Trail.

Developers and QA teams can open the issue report in the Appxiom dashboard and immediately inspect the exact breadcrumb trail leading up to the failure.

The Four Pillars of the Activity Trail​

Unlike ad-hoc logging solutions that require writing boilerplate logging statements across every view, the Activity Trail is built on four automated capabilities:

1. Auto-Tracked User Interactions​

Appxiom automatically captures UI touch events - clicks, taps, button presses, and gestures - without requiring developers to manually bind click listeners or insert logging hooks.

When a customer encounters a regression, you don't have to wonder whether they tapped "Submit", "Cancel", or the back arrow. The Activity Trail explicitly records:

  • App:TextButton - 'view order' tapped
  • App:ElevatedButton - 'Get Location' tapped
  • App:ElevatedButton - 'Place Order' tapped

2. Screen Navigation & Lifecycle Transitions​

Mobile state corruption almost always stems from screen transitions and lifecycle changes. Appxiom automatically records:

  • Activity and Fragment lifecycle events in Android (onCreate, onStart, onResume, onPause, onDestroy).
  • UIViewController lifecycle events in iOS (viewDidLoad, viewWillAppear, viewDidAppear).
  • Widget and component mounts in Flutter and React Native.
  • Foreground/background transitions (e.g., when the user switches apps or locks their screen).

3. Environment & Network State Changes​

Network connectivity on mobile devices is inherently unstable. Appxiom monitors device connectivity state changes and captures:

  • When the device drops from Wi-Fi to Cellular or goes completely offline.
  • When connectivity is restored.
  • Outgoing HTTP and HTTPS requests, including target endpoints, HTTP methods, latency in milliseconds, and status codes (e.g., 200 OK (86ms) or 404 Not Found (142ms)).

4. Custom Developer Markers (Ax.setActivityMarker)​

While auto-tracking captures the technical and UI baseline, developers often need to stamp domain-specific business context into the trail.

Appxiom provides a clean, single-line API that can be called anywhere in your codebase:

// Android (Kotlin)
Ax.setActivityMarker(this@CheckoutActivity, "checkout_step_coupon_applied: SUMMER26")
// iOS (Swift)
Ax.setActivityMarkerAt(self, marker: "biometric_authentication_verified")
// Flutter
Ax.setActivityMarker("cart_recalculated_with_tax: $total");

These custom markers are woven directly into the chronological stream alongside taps, screen transitions, and HTTP calls.

Activity Trail vs. Heavy Session Replay: Why Lightweight Breadcrumbs Win​

A common question engineers ask is: "Why not just use a full session replay tool like LogRocket, FullStory, or Sentry Session Replay?"

While session replay video recordings sound appealing on paper, engineering teams frequently abandon them in mobile production for several critical reasons:

Feature / ConsiderationHeavy Session Replay ToolsAppxiom Activity Trail
CPU & Frame Rate ImpactHigh (captures DOM/Canvas frames, causes UI jank)Zero (idle-frame execution, smooth 60/120fps)
Network & Battery DrainHeavy (megabytes of compressed video/wireframes)Negligible (ultra-compressed text payloads)
Data Privacy & PII RiskHigh risk of leaking passwords, cards, or user dataZero PII by design (captures events, not inputs)
Offline ResilienceBuffers fail or drop video chunks on bad networksSandboxed local cache with seamless resend
Engineering SignalRequires watching 5-minute videos to find a bugInstant 10-second scan of the exact causal chain

Activity Trail gives developers the precise technical forensics they need to fix code without the privacy nightmares, compliance headaches, or performance penalties of video replays.

Visualizing the Crime Scene: Chronological Activity Trail Walkthrough​

Below is an example of what an engineer sees in the Appxiom dashboard when inspecting an issue report. Notice how each event includes a microsecond-accurate timestamp, event category, event payload, and the device's free memory percentage at that exact moment:

APPXIOM ACTIVITY TRAIL TIMELINE

Issue: POST /api/v2/orders/process → 409 Conflict
Exception: IllegalStateException: Order already locked
Version: v4.2.0 (Build 184) | Device: Google Pixel 8 | OS: Android 15

TIMECATEGORYEVENT DETAILSFREE MEM
10:14:02.115LIFECYCLECartActivity:onResume48.2%
10:14:03.450USER ACTIONClicked "Apply Promo Code"48.0%
10:14:03.538HTTPPOST /api/v2/promos/validate → 200 OK (88ms)47.9%
10:14:04.102CUSTOMAx.setActivityMarker: promo_applied_SUMMER2647.9%
10:14:06.890USER ACTIONClicked "Proceed to Checkout"47.5%
10:14:07.012LIFECYCLECheckoutActivity:onCreate46.8%
10:14:07.410LIFECYCLECheckoutActivity:onResume46.8%
10:14:08.115NETWORKWIFI → CELLULAR_OFFLINE46.8%
10:14:08.840USER ACTIONClicked "Place Order" (Tap 1)46.5%
10:14:09.120USER ACTIONClicked "Place Order" (Tap 2)46.5%
10:14:09.650NETWORKCELLULAR_OFFLINE → 4G_ONLINE46.4%
10:14:10.200HTTPPOST /api/v2/orders/process → 200 OK (110ms)46.2%
10:14:10.315HTTPPOST /api/v2/orders/process → 409 Conflict (98ms)46.2%
10:14:10.420ISSUEIllegalStateException: Cannot process duplicate46.1%

Dissecting the Crime Scene in Under 60 Seconds​

Without the Activity Trail, all the engineer would see is a stack trace showing IllegalStateException: Cannot process duplicate order at CheckoutActivity.kt:188.

The engineer would spend days trying to understand why duplicate orders are being sent. Is the backend caching stale idempotency tokens? Is Retrofit duplicating headers?

With the Activity Trail, the answer is immediately obvious:

  1. At 10:14:08.115, the user's connection dropped to offline.
  2. The user tapped 'Place Order'. Nothing visibly happened because the request was queued.
  3. Frustrated, the user tapped 'Place Order' a second time at 10:14:09.120.
  4. Half a second later, network connectivity was restored.
  5. Both queued requests fired simultaneously. The first succeeded (200 OK), but the second was rejected by the server (409 Conflict).
  6. The client code lacked button debouncing and did not handle the 409 conflict gracefully, triggering an unhandled exception.

Time to diagnose: 45 seconds.
Fix: Add a UI debounce on the checkout button and catch 409 status responses to show an idempotent confirmation banner.

Solving Three Infuriating Release Regressions with Activity Trail​

Let's look at three notorious categories of regressions that plague engineering teams and how Activity Trail resolves them.

Case 1: State Restoration & Race Conditions After App Backgrounding​

The Bug: Users complain that after receiving a phone call or switching to their banking app to copy a one-time password (OTP), returning to your app causes a blank screen or a crash.

Why It's a Nightmare to Reproduce: On developer devices, Android and iOS have ample RAM. When you switch apps during testing, your application remains alive in memory. But in production, users with 20 background apps, low-end devices, or aggressive OEM battery optimizers experience immediate process death.

How Activity Trail Solves It: The trail clearly records:

14:22:10.100  LIFECYCLE    PaymentActivity:onPause
14:22:10.350 LIFECYCLE PaymentActivity:onStop
14:22:10.400 LIFECYCLE App went to BACKGROUND
14:22:35.800 LIFECYCLE App returned to FOREGROUND
14:22:36.100 LIFECYCLE PaymentActivity:onCreate (savedInstanceState != null)
14:22:36.210 ISSUE NullPointerException: ViewModelFactory not initialized

The developer instantly realizes: the activity restoration flow assumed the DI singleton was still warm. When the OS killed the process in the background, onCreate ran with a non-null savedInstanceState, but the singleton was re-initialized to null.

Case 2: Silent ANRs and UI Freezes on the Main Thread​

The Bug: Google Play Console flags a spike in your ANR rate, threatening your app's search visibility and store ranking. The trace provided by Google Play Vitals simply says:

"main" prio=5 tid=1 Native
#00 pc 000000000004b344 /apex/com.android.runtime/lib64/bionic/libc.so (syscall+28)
#01 pc 00000000001ae4f8 /apex/com.android.art/lib64/libart.so (art::ConditionVariable::WaitHoldingLocks)

This tells you the main thread was waiting on a lock. Which lock? Triggered by what? Google Play Vitals cannot tell you.

How Activity Trail Solves It: In Appxiom, the ANR issue report displays the chronological trail right before the freeze:

12:22:50.110  USER ACTION  Clicked - button - 'Filter Transactions'
12:22:50.215 CUSTOM Ax.setActivityMarker: filter_dataset_size_45000_items
12:22:50.220 LIFECYCLE TransactionListFragment:onPause
12:22:55.300 ISSUE ANR Detected: Main thread blocked for 5,080ms

The correlation is unmistakable: the user clicked 'Filter Transactions' with a dataset of 45,000 items. The sorting operation ran synchronously on the main thread instead of being offloaded to Dispatchers.Default or a background worker.

Case 3: Progressive Memory Leaks Disguised as Out-Of-Memory Crashes​

The Bug: You see an OutOfMemory (OOM) crash on a simple profile update screen. Developers insist: "There's no way updating an avatar consumes 512 MB of heap!"

How Activity Trail Solves It: Because Appxiom records the free memory percentage with every single lifecycle event and marker, you can trace the memory trajectory over time:

09:15:10  ProductDetailActivity:onCreate       54.2% FREE MEMORY
09:15:32 ProductDetailActivity:onDestroy 48.1% FREE MEMORY
09:16:04 ProductDetailActivity:onCreate 41.3% FREE MEMORY
09:16:28 ProductDetailActivity:onDestroy 35.0% FREE MEMORY
09:17:02 ProductDetailActivity:onCreate 28.4% FREE MEMORY
09:17:40 ProductDetailActivity:onDestroy 21.1% FREE MEMORY
09:18:12 ProfileActivity:onCreate 12.5% FREE MEMORY
09:18:15 ISSUE: java.lang.OutOfMemoryError 4.1% FREE MEMORY

The profile screen didn't cause the leak. The ProductDetailActivity was failing to unregister an image caching listener, leaking 30 MB of bitmap memory every time it was opened. The profile screen was merely the unlucky innocent bystander when memory ran out.

Instrumenting Activity Trail Across Android, iOS, and Flutter​

Instrumenting your application with Appxiom doesn't require rewriting your codebase. The SDK is engineered for effortless setup and zero friction.

Android (Kotlin & Java)​

import com.appxiom.android.Ax

class CheckoutActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_checkout)

// Custom marker that appears in the Activity Trail
Ax.setActivityMarker(this, "entered_checkout_flow")

// Debug-only marker (automatically stripped from release builds)
Ax.setActivityMarkerForDebug(this, "debug_cart_hash: ${cart.hashCode()}")
}
}

ProGuard & R8 Protection: When using aggressive code shrinking and obfuscation, class names in standard crash reports turn into unreadable alphabet soup (a.b.c). Appxiom provides the @AX annotation to maintain human-readable identifiers in your Activity Trail even in release builds:

@AX("CheckoutScreen")
class CheckoutActivity : AppCompatActivity() { ... }

iOS (Swift & SwiftUI)​

import Appxiom

class PaymentViewController: UIViewController {

override fun viewDidLoad() {
super.viewDidLoad()

// Single-line marker injection
Ax.setActivityMarkerAt(self, marker: "stripe_payment_sheet_presented")
}

@IBAction func didTapPayButton(_ sender: UIButton) {
Ax.setActivityMarkerAt(self, marker: "pay_button_pressed")
processPayment()
}
}

Flutter (Dart)​

import 'package:appxiom_flutter/appxiom_flutter.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

// Initialize Appxiom for Android & iOS
Ax.init("android_app_key", "android_platform_key");
Ax.initIOS();

// Wrap app runner for automatic widget and navigation tracking
AxApp.run(
child: const MyApp(),
screenName: 'MainApp',
widgetName: 'MyApp',
);
}

// Anywhere in your business logic:
Ax.setActivityMarker("user_applied_filter_category_electronics");

Production Reliability: Idle-Frame Processing, Offline Sandboxing, and Zero-PII Privacy​

Mobile engineers are rightly skeptical of third-party SDKs that monitor user interactions. If a diagnostic tool causes frame drops or drains the battery, it becomes part of the problem.

Appxiom was designed with three strict production guarantees:

1. Idle-Frame Execution​

Data collection, timestamp calculation, and event buffering run strictly during CPU idle frames. Event batching never interrupts the UI rendering pipeline, ensuring your 60fps and 120fps ProMotion animations remain buttery smooth.

2. Offline Sandbox Buffering​

Mobile users enter tunnels, board subways, and toggle airplane mode. When network connectivity drops, Appxiom securely caches breadcrumbs and activity markers in a local sandbox. Once connectivity is re-established, the data is compressed, batched, and transmitted in the background without dropping a single event.

3. Privacy-First by Design​

Appxiom tracks interaction geometry, button identifiers, and component lifecycles. It never captures sensitive user keystrokes, passwords, or PII. You get the technical context required to reproduce the defect without compromising user privacy or violating GDPR and CCPA compliance.

Beyond Isolated Fixes: Version Analytics, Quality Score, and Release Regression Prevention​

The Activity Trail is a powerful debugging tool for individual bugs, but its true superpower emerges when connected to Appxiom's full observability suite:

  1. Appxiom Quality Score (QS): Evaluates release health on a unified 0–10 scale across crashes, ANRs, app hangs, and latency. If your Quality Score drops immediately after a release, you know a regression has hit production.
  2. Goal Friction Impact (GFI): Quantifies exactly how much money and conversion volume a regression is costing your business by measuring drop-offs in critical funnels (signups, subscriptions, purchases).
  3. Version Analytics: Compares error distributions across releases to verify whether a hotfix genuinely resolved the targeted regression without introducing secondary anomalies.
  4. Activity Trail: Provides the engineering team with the exact user footsteps needed to replicate, diagnose, and fix the root cause immediately.

Instead of arguing in cross-functional meetings about whether a bug is reproducible or how severe it is, engineering, QA, and product teams have a shared, unambiguous timeline of reality.

Stop Guessing. Start Reproducing.​

Every hour your engineering team spends guessing how an edge case happened is an hour stolen from building features that grow your product.

Every ticket closed as "Cannot Reproduce" is a ticking time bomb waiting to detonate in your app store ratings.

With Appxiom's Activity Trail, you never have to ask:

  • "What did the user click before it crashed?"
  • "Can QA get a screen recording?"
  • "Why doesn't this happen on my machine?"

You open the issue, read the trail, and fix the bug.

Ready to Eliminate "Cannot Reproduce" From Your Sprints?​

Stop letting release regressions derail your team's momentum. See how Appxiom's Activity Trail, Quality Score, and Goal Friction Impact give mobile teams superpowers.

Start Your Free Appxiom Trial Today or explore our comprehensive documentation to see how easy integration really is.

App Hangs in iOS: Causes, Code Fixes, and How to Spot Them

Published: · Last updated: · 5 min read
Don Peter
Cofounder and CTO, Appxiom

Ever tapped a button in your app and waited... and waited... until you started questioning your life choices?

Yeah, that's an app hang.

It's not a crash. It's worse. Your app doesn't explode, it just freezes. Quietly. Awkwardly. Like someone forgot their lines on stage and now the whole audience is staring.

App hangs are sneaky. They don't always show up in crash reports. But your users feel them. In the lags, the unresponsive screens, the moments when they swipe but nothing moves. And if it happens too often? That uninstall button starts looking real attractive.

But it doesn't have to be that way.

Let's fix the freeze before the curtain falls.

Solving Frame Rate Issues and App Hangs in SwiftUI iOS Apps

Published: · Last updated: · 7 min read
Appxiom Team
Mobile App Performance Experts

Developing a smooth and responsive iOS app is crucial for providing a great user experience. Frame rate issues and app hangs can be frustrating for users and can lead to negative reviews and decreased app usage.

In this blog post, we will explore common causes of frame rate issues and app hangs in SwiftUI iOS apps and provide solutions and code examples to address them.

Understanding Frame Rate Issues​

Frame rate issues occur when an app struggles to render frames at the desired rate, usually 60 frames per second (FPS) on most iOS devices. When the frame rate drops, animations become choppy, and the app feels less responsive. There are several common reasons for frame rate issues:

  • Inefficient View Updates: SwiftUI's declarative nature encourages frequent view updates. If not optimized, this can lead to excessive rendering and reduced frame rates.

  • Heavy Computation on the Main Thread: Performing CPU-intensive tasks on the main thread can block the UI, making the app feel unresponsive.

  • Large Images and Assets: Loading or rendering large images or assets can consume significant memory and processing power, leading to frame rate drops.

Solving Frame Rate Issues in SwiftUI​

1. Optimize View Updates​

You can optimize view updates by:

  • Using the .onAppear and .onDisappear modifiers to load data only when necessary.

  • Implementing the .id modifier to identify views uniquely and avoid unnecessary updates.

  • Reducing the complexity of SwiftUI view hierarchies.

Example:

struct ContentView: View {
var body: some View {
Text("Optimize your views")
.onAppear {
// Load data when the view appears
loadData()
}
}
}

​

2. Offload Heavy Computation​

Move CPU-intensive tasks to background threads using DispatchQueue or Combine. Ensure that UI updates occur on the main thread.

Using DispatchQueue:

DispatchQueue.global().async {
// Perform heavy computation
let result = performHeavyComputation()

DispatchQueue.main.async {
// Update the UI on the main thread
self.resultLabel = result
}
}

Combine is a powerful framework for handling asynchronous and event-driven code in Swift. You can use Combine to perform background operations in SwiftUI seamlessly. In this example, we'll demonstrate how to use Combine to execute a background operation and update the SwiftUI view when the operation completes.

Let's say you want to fetch some data from a network API in the background and update your SwiftUI view when the data is ready. Here's a step-by-step guide:

  1. Import Combine in your SwiftUI view file:
import SwiftUI
import Combine
  1. Define a ViewModel to handle your data and background operations. Create an ObservableObject class that will hold your data and expose a publisher for notifying view updates.
class MyViewModel: ObservableObject {
@Published var data: [YourDataType] = [] // Replace YourDataType with the actual data type you're using
private var cancellables: Set<AnyCancellable> = []

func fetchData() {
// Simulate a background network request
fetchDataFromNetwork()
.receive(on: DispatchQueue.main) // Ensure updates are on the main thread
.sink { completion in
// Handle completion or errors if needed
} receiveValue: { [weak self] newData in
self?.data = newData
// Update the data when received
}
.store(in: &cancellables)
}

private func fetchDataFromNetwork() -> AnyPublisher<[YourDataType], Error> {
// Implement your network request logic here and return a Combine publisher
// For example, you can use URLSession's dataTaskPublisher
let url = URL(string: "https://your-api-url.com/data")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [YourDataType].self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}

Replace YourDataType with the actual type of data you're fetching from the network.

  1. Create a SwiftUI view that observes the changes in your ViewModel and triggers the background operation:
struct ContentView: View {
@ObservedObject private var viewModel = MyViewModel()

var body: some View {
VStack {
if viewModel.data.isEmpty {
Text("Loading...")
} else {
List(viewModel.data, id: \.self) { item in
// Display your data here
Text(item.name)
// Replace with your data properties
}
}
}
.onAppear {
viewModel.fetchData()
// Trigger the background operation when the view appears
}
}
}

In this SwiftUI view, the @ObservedObject property wrapper observes changes to the viewModel, and the onAppear modifier triggers the background operation by calling viewModel.fetchData() when the view appears.

Now, your SwiftUI view will fetch data from the network in the background using Combine and update the view when the data is ready, providing a smooth and responsive user experience.

3. Efficiently Manage Images and Assets​

Load images lazily and use asset catalogs for managing image resources. Resize images to appropriate dimensions to reduce memory usage.

In SwiftUI, you can load images lazily using the AsyncImage view. AsyncImage allows you to load and display images asynchronously, which is especially useful for large images or images fetched from the network. Here's how you can use AsyncImage to load images lazily in SwiftUI:

import SwiftUI

struct LazyLoadingImageView: View {
let imageURL: URL
var body: some View {
AsyncImage(url: imageURL) { phase in
switch phase {
case .empty:
// Placeholder while loading (optional)
ProgressView()
case .success(let image):
// Successfully loaded image
image
.resizable()
.scaledToFit()
case .failure(_):
// Handle the failure (e.g., show an error message)
Image(systemName: "xmark.octagon")
.resizable()
.scaledToFit()
.foregroundColor(.red)
@unknown default:
// Handle other unknown states
Text("Unknown state")
}
}
}
}

In the code above:

  • AsyncImage is used to load the image asynchronously from the specified URL.

  • The closure inside AsyncImage receives a Phase parameter, which represents the current state of the image loading process.

  • In the .empty phase, you can display a placeholder (e.g., a ProgressView) to indicate that the image is being loaded.

  • In the .success phase, you can display the loaded image, making it resizable and scaling it to fit the available space.

  • In the .failure phase, you can handle the failure by displaying an error image or a message.

  • The @unknown default case is used to handle any unknown states that might be introduced in future SwiftUI versions.

To use the LazyLoadingImageView in your SwiftUI view, simply provide the URL of the image you want to load:

struct ContentView: View {
var body: some View {
LazyLoadingImageView(imageURL: URL(string: "https://example.com/image.jpg")!)
.frame(width: 200, height: 200)
}
}

Make sure to replace "https://example.com/image.jpg" with the actual URL of the image you want to load.

With AsyncImage, you can efficiently load and display images in a lazy manner, ensuring a smooth user experience, especially when dealing with large images or images from remote sources.

Addressing App Hangs​

App hangs occur when the app becomes unresponsive for 250 milli seconds or more due to various reasons, such as blocking the main thread or network requests taking too long. Here are some strategies to prevent app hangs:

1. Use Background Threads for Network Requests​

Perform network requests on background threads to avoid blocking the main thread. Combine or URLSession can be used for this purpose.

Example:

let cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in }) { data in
// Process data and update UI
}

2. Implement Error Handling​

Handle errors gracefully, especially in asynchronous operations, to prevent app hangs and crashes.

do {
let result = try performRiskyOperation()
// Handle the result
} catch {
// Handle the error
}

​

3. Use Xcode Instruments and APM tools​

Use Xcode's Instruments to profile your app's performance, identify bottlenecks, and monitor memory usage. Debugging tools like LLDB can help trace and fix specific issues causing frame rate issues and app hangs.

APM tools are a very much helpful in detecting frame rate issues and App Hangs. Appxiom is such an APM tool that helps in detecting these issues and provides all relevant data points to the developer to fix the issue.

Conclusion​

Frame rate issues and app hangs can significantly impact the user experience of your SwiftUI iOS app. By optimizing view updates, offloading heavy computation, efficiently managing assets, and addressing app hangs through proper threading and error handling, you can create a smooth and responsive app that users will love.

Remember that performance optimization is an ongoing process. Regularly test your app on different devices and keep an eye on performance metrics to ensure a consistently great user experience.

Reasons for App Hangs in iOS and How to Fix Them

Published: · Last updated: · 4 min read
Appxiom Team
Mobile App Performance Experts

App hangs or freezes are common issues faced by iOS users and can be frustrating for both developers and users. An app hang occurs when an application becomes unresponsive for more than 250 milliseconds, leading to a poor user experience.

In this blog post, we will explore some common reasons for app hangs in iOS and discuss effective solutions to fix them.

Reasons for App Hangs in iOS​

1. Long-Running Tasks on the Main Thread​

The main thread in iOS is responsible for handling user interactions and updating the user interface. Performing long-running tasks on the main thread can cause the app to freeze and become unresponsive. Examples of long-running tasks include network requests, database operations, or complex computations.

Solution: Move long-running tasks to background threads using Grand Central Dispatch (GCD) or Operation Queues. By doing so, the main thread remains free to handle user interactions, ensuring a smooth user experience.

Here's an example using GCD

DispatchQueue.global(qos: .background).async {
// Perform your long-running task here
DispatchQueue.main.async {
// Update UI on the main thread if necessary
}
}

2. Excessive CPU or Memory Usage​

If an app consumes excessive CPU or memory resources, it can lead to poor performance and potential app hangs. Memory leaks, retain cycles, or inefficient resource management are common causes of high resource usage.

Solution: Use Instruments, a powerful profiling tool in Xcode, to analyze and optimize your app's CPU and memory usage. Address any memory leaks, properly release resources, and optimize algorithms to reduce resource consumption.

3. UI Blocking Operations​

Performing operations that block the main thread can cause the app to hang. For instance, synchronous network requests or disk I/O operations can lead to unresponsiveness.

Solution: Utilize asynchronous APIs and techniques to prevent blocking the main thread. For network requests, use frameworks like Alamofire or URLSession with completion handlers or async/await for async APIs. For disk I/O, employ background queues or DispatchQueue.async.

4. Deadlocks and Race Conditions​

Deadlocks occur when multiple threads are waiting for each other to release resources, resulting in a complete halt. Race conditions arise when multiple threads access shared resources simultaneously, leading to unpredictable behavior and app hangs.

Solution: Use synchronization techniques like locks, semaphores, or dispatch barriers to handle shared resources safely. Carefully review and analyze your code for potential deadlocks and race conditions. Utilize tools like Thread Sanitizer in Xcode to detect and fix such issues.

5. Infinite Loops​

An infinite loop occurs when a section of code keeps executing indefinitely, preventing the app from responding.

Solution: Thoroughly review your code for any infinite loops and ensure appropriate loop termination conditions are in place. Use breakpoints and debugging tools to identify and fix such issues during development.

Using APM Tools to Detect and Identify App Hangs​

In addition to following the aforementioned solutions, leveraging APM tools can be immensely helpful in identifying and diagnosing the root cause of app hangs. Two popular APM tools for iOS are Firebase and Appxiom.

1. Firebase Performance Monitoring​

Firebase Performance Monitoring is a comprehensive APM tool provided by Google. It allows you to gain insights into your app's performance, including metrics related to app hangs, slow rendering, network requests, and more.

2. Appxiom​

Appxiom is another powerful APM tool specifically designed for iOS and Android applications. It offers deep insights into app performance, including identifying bottlenecks, detecting crashes, and diagnosing app hangs.

Conclusion​

App hangs in iOS can be caused by various factors such as long-running tasks on the main thread, excessive CPU or memory usage, UI blocking operations, deadlocks, race conditions, and infinite loops. By understanding these reasons and implementing the suggested solutions, you can significantly improve your app's responsiveness and provide a better user experience.

Additionally, by utilizing APM tools like Firebase and Appxiom, you can detect and identify the root cause of app hangs more effectively. These tools offer detailed insights, performance metrics, and real-time monitoring to help you optimize your app's performance and address hang-related issues promptly.

Remember to test your app thoroughly on different devices and iOS versions to ensure its stability and responsiveness. Regularly profiling and optimizing your app's performance will help you catch and resolve potential hang issues early in the development cycle.

By following best practices, utilizing appropriate tools, and adopting efficient coding techniques, you can mitigate app hangs and deliver a seamless experience to iOS users.

Happy coding!

Frame Rate Issues in Flutter Apps and How to Solve Them

Published: · Last updated: · 4 min read
Appxiom Team
Mobile App Performance Experts

Flutter, Google's open-source UI development framework, has gained immense popularity among developers for its cross-platform capabilities and smooth performance. However, like any software development framework, Flutter apps may encounter frame rate issues that can impact user experience.

In this blog, we will explore the common causes of frame rate issues in Flutter apps and provide effective solutions to mitigate them.

Understanding Frame Rate Issues in Flutter Apps​

The frame rate of a Flutter app refers to the number of frames or screen updates displayed per second. The standard frame rate for smooth user experience is 60 frames per second (fps). If an app fails to achieve this frame rate consistently, it can result in stuttering animations, sluggish responsiveness, and an overall degraded user experience.

In Android, frame rate issues may manifest as App Not Responding (ANR) if the UI Thread gets blocked for 5000 milliseconds or more. If the UI Frames take 700 milliseconds or more to render it is a Frozen Frame situation and if it takes 16 milliseconds or more it is a Slow Frame situation.

In iOS, if the UI Thread is stuck for 250 milliseconds or more it is an App Hang, also called App Freeze, situation.

Common Causes of Frame Rate Issues​

1. Expensive Widget Rebuilds​

class MyExpensiveWidget extends StatelessWidget {
final ExpensiveData data;

const MyExpensiveWidget({required this.data});

@override
Widget build(BuildContext context) {
// Widget build logic that might be expensive
return ...;
}
}

To optimize widget rebuilds, use const constructors whenever possible. By using const, Flutter can efficiently skip the widget rebuild if the constructor parameters haven't changed.

2. Inefficient Animations​

​

class MyAnimationWidget extends StatefulWidget {
@override
_MyAnimationWidgetState createState() => _MyAnimationWidgetState();
}

class _MyAnimationWidgetState extends State<MyAnimationWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween(begin: 0.0, end: 1.0).animate(_controller);
_controller.forward();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// Widget build logic using the animation value
return ...;
},
);
}
}

To optimize animations, use lightweight animations like Tween animations instead of heavy ones like Hero animations. Properly dispose of animation controllers to release resources and avoid unnecessary computations. Implement animation caching techniques, such as pre-loading and reusing animations, to reduce performance impact.

3. Inadequate Caching and Data Fetching​

​

class MyDataFetcher {
static final Map<String, dynamic> _cache = {};

static Future<dynamic> fetchData(String url) async {
if (_cache.containsKey(url)) {
return _cache[url];
} else {
final response = await http.get(Uri.parse(url));
final data = json.decode(response.body);
_cache[url] = data;
return data;
}
}
}

To optimize caching and data fetching, implement proper caching strategies. Utilize Flutter's built-in caching mechanisms, such as cached_network_image, to minimize repeated image downloads. Implement pagination techniques to fetch data incrementally instead of in one large chunk.

4. Simplify Layouts​

​

class MyComplexLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Expanded(
child: Row(
children: [
Flexible(child: Container()),
Flexible(child: Container()),
],
),
),
Expanded(
child: Container(),
),
],
),
);
}
}

To simplify layouts, minimize nested layouts and unnecessary constraints. Use appropriate layout widgets based on specific requirements. Avoid excessive use of Expanded and Flexible widgets when other layout techniques like SizedBox or AspectRatio can achieve the desired results.

Use App Performance Monitoring (APM) Tools​

Monitoring the frame rate of a Flutter app is crucial for maintaining optimal performance and delivering a smooth user experience. APM tools provide valuable insights into the app's rendering performance, allowing developers to identify and address frame rate issues effectively.

Two widely used tools for frame rate monitoring in Flutter are Firebase Performance Monitoring and Appxiom.

Conclusion​

Frame rate issues in Flutter apps can negatively impact the user experience, leading to reduced engagement and user satisfaction. By optimizing widget rebuilds, animations, caching and data fetching, as well as simplifying layouts, developers can ensure a smooth and responsive UI.

Remember to profile your app, optimize animations, simplify layouts, and follow best practices to address frame rate issues effectively. Use APM tools to continuously monitor app performance including frame rate issues. With careful attention to performance optimization, Flutter can deliver exceptional user experiences across various platforms.

Performance Testing of iOS Apps

Published: · Last updated: · 4 min read
Appxiom Team
Mobile App Performance Experts

Performance testing is a critical aspect of iOS app development. It ensures that the app performs optimally, providing a seamless user experience. With millions of apps available in the App Store, it is imperative that an iOS app must perform well to succeed.

In this blog, we will explore what iOS app performance testing is, the best practices to follow, and the tools available.

What is iOS App Performance Testing?​

iOS app performance testing is the process of testing an application's performance and behavior on iOS devices. The testing process includes evaluating the app's response time, speed, stability, scalability, and resource utilization. The goal of iOS app performance testing is to identify any performance issues before the app is released to the public.

What to test?​

  • Memory usage including memory leaks, abnormal memory usage, memory spikes.

  • Battery drain

  • CPU usage

  • Network call performance issues, Error status codes in responses, delayed calls, duplicate calls.

  • App Hang

  • Screen responsiveness

  • User flow and logic

Steps in iOS App Performance Testing​

  • Define Test Objectives - The first step in iOS app performance testing is to define the test objectives. This includes identifying the target audience, user scenarios, and performance goals.

  • Identify Performance Metrics - The next step is to identify the performance metrics that need to be tested. This includes response time, speed, stability, scalability, and resource utilization.

  • Create Test Environment - The test environment should be created to simulate real-life scenarios. This includes configuring the hardware and software components, network conditions, and device settings.

  • Develop Test Plan - A detailed test plan should be developed, outlining the test scenarios, test cases, and expected results.

  • Execute Test Plan - The test plan should be executed as per the defined scenarios, and the app's performance should be evaluated under different conditions.

  • Analyze Test Results - The test results should be analyzed to identify performance issues and bottlenecks.

  • Optimize App Performance - Based on the test results, the app's performance should be optimized to ensure that it meets the performance goals and objectives.

Tools for iOS App Performance Testing​

  • Xcode Instruments - Xcode Instruments is a powerful tool that can be used for iOS app performance testing. It provides a wide range of profiling and debugging tools that can help identify and resolve performance issues.

  • Charles Proxy - Charles Proxy is a tool that can be used to monitor network traffic, including HTTP and SSL traffic. It can be used to test the app's performance under different network conditions.

  • XCTest - XCTest is an automated testing framework provided by Apple for testing iOS apps. It can be used to create automated performance tests.

  • Firebase Test Lab - Firebase Test Lab is a cloud-based testing platform that provides a wide range of testing capabilities, including performance testing.

  • BrowserStack - Cloud based testing platform with a range of features to identify and debug issues while testing.

  • Appxiom - SaaS platform that reports performance issues and bugs in iOS apps in real time. It detects Memory issues, screen responsiveness, crashes, rendering issues, network call issues over HTTP and HTTPS and much more in development, testing and live phases of the app.

Best Practices for iOS App Performance Testing​

  • Test Early and Often - iOS app performance testing should be an integral part of the development process, and testing should be done early and often.

  • Use Real Devices - Testing should be done on real devices to simulate real-life scenarios accurately.

  • Define Realistic Test Scenarios - Test scenarios should be defined based on real-life scenarios to ensure that the app's performance is tested under realistic conditions.

  • Use Automated Testing - Automated testing should be used to reduce the testing time and improve accuracy.

  • Monitor App Performance - App performance should be monitored continuously to identify any performance issues and bottlenecks.

  • Collaborate with Developers - Collaboration between testers and developers can help identify and resolve performance issues early in the development process.

Conclusion​

iOS app performance testing ensures that the app performs optimally, providing a seamless user experience. By following best practices and using the right tools, iOS app developers can identify and resolve performance issues early in the development process, resulting in a high-quality app that meets the user's expectations. It is essential to test the app's performance under different conditions to ensure that it performs well under all circumstances. Therefore, app performance testing should be an integral part of the iOS app development process.

Why Mobile App Testers and Developers Should Use APM Tools for Performance Monitoring.

Published: · Last updated: · 2 min read
Appxiom Team
Mobile App Performance Experts

Performance monitoring and continues bug monitoring are critical parts of the Mobile App development lifecycle. As mobile devices become more powerful and users expect more from their apps, it is essential to ensure that apps are performing well and are free of bugs. One way to achieve this is by using Application Performance Management (APM) tools.

APM tools are designed to help mobile app testers and developers detect and diagnose performance issues and bugs in their apps. These tools provide a wide range of information about an app's performance, including memory usage, CPU usage, network activity, and more. This information can be used to identify bottlenecks, memory leaks, and other issues that can negatively impact an app's performance.

One of the main benefits of using APM tools is that they can help app developers and testers find and fix performance issues before they become a problem for users. By identifying issues early in the development process, teams can make changes to improve performance and ensure that the app is stable and reliable. This can help reduce the number of crashes and improve the overall user experience.

Another benefit of using APM tools is that they can help developers and testers understand how users are interacting with their apps. This can be especially useful for understanding how different user segments are interacting with the app, which can help teams optimize the user experience and make improvements that will have the biggest impact.

In short, APM tools are an essential tool for mobile app testers and developers. They help teams identify and fix performance issues and bugs, improve the user experience, and ensure that apps are stable and reliable. By using APM tools, teams can deliver better quality apps and create a more positive user experience.

Visit appxiom.com to know more about how Appxiom can help you with monitoring performance and bugs in mobile apps.

How to Detect App Hangs in iOS Apps, and Fix Them.

Published: · Last updated: · 4 min read
Don Peter
Cofounder and CTO, Appxiom

One major objective for any app developer is to make sure that their iOS app is smooth and responsive. So, how does one make sure the app is responsive? The rule of thumb is that an application should be able to react to user inputs and touches within 250ms.

If the response time is above 250 ms then the delay becomes apparent and will be noticeable to the app user. iOS documentation categorizes any app response delay that persists for more than 250 ms as App hang.

What developers need is a proper way to identify and fix App hangs. XCode organizer and bug reporting tools like MetricKit and Appxiom report App hangs. Reports generated by xcode organizer and MetricKit are not real-time and they also miss App Hangs at times.

Appxiom reports App hangs that occur in the application in real-time and supports development, Test Flight and App Store builds. Because the reports are real-time, most App Hang situations will be captured.

Analyzing an App Hang Report​

Here are some of the metrics needed for deeper analysis from an App hang report generated by Appxiom,

  • Top iOS versions affected.

  • Top device models affected.

  • First app version where the issue is detected.

  • Number of occurrences of the issue.

  • Total time for which the app was unresponsive.

  • Stack trace to identify the point of hang.

Appxiom provides App hang issue reports with these metrics.

Top OS versions, Device models and countries where App hang was reportedNumber of Devices and total occurrence count of App hang issuePrevious app versions, if any, where the same App hang issue was reportedIn most cases the stack trace provided with the issue should give indication of where the root cause lies.

Stack-trace indicating the point of origin of App hang issueThere could be situations where stack trace alone might not be sufficient. This is when the rest of the metrics help.

Look for patterns like the top OS versions, device models and the app version where this issue first occurred in the issue report. Occurrences count of the issue will provide a sense of how severe the issue is. Using the combination of these, developers will be able to prioritize and create the test device configuration to reproduce the issue.

Next step is to retrace how the user interacted with the app when the issue occurred. Activity trail, which is a chronologically ordered list of events that happened prior to the App hang which is provided with each App hang issue report in Appxiom dashboard. It will have events like the lifecycle of the different viewcontrollers, network changes and state changes of the app along with custom events that are set by the developer.

Activity trail listing events that occurred prior to App hang issue.This will help developers to retrace the app events to identify and reproduce the issue.

How to avoid App Hang situations.​

Now that we know how to detect and fix App hangs, let us explore ways to prevent App hangs.

In order to achieve this the main thread of the app should be free from running intensive operations as the application gets frozen when the main thread is stuck.

Now what this means is, execute only tasks that interact with the user interface in the main thread i.e UIKit, AppKit, or SwiftUI based tasks. So how does one execute other long running tasks? Use background threads directly or either through the operation queue or through the Grand Central Dispatch queue to execute such operations.

Let us take the example of a simple HTTP(s) request and using its response data to update a tableview in the app. It is not just the HTTP(s) request that we execute in the separate thread, but de-serializing the JSON object from the response data should be executed outside of the main thread as well. Only the actual tableview update code needs to be executed in the main thread.

Following this coding practice will lower the possibility of App hang issues in your application.

Appxiom is available for Objective-C and Swift in iOS and watchOS, and for Java and Kotlin in Android. Click here to get started.