Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions LoopFollow/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
LogManager.shared.log(category: .general, message: "App started")
LogManager.shared.cleanupOldLogs()
ExitDiagnostics.shared.start()

// Notification and calendar permissions are no longer requested here.
// They're deferred to the moment the user opts into the feature that
Expand Down Expand Up @@ -109,6 +110,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}

func applicationWillTerminate(_: UIApplication) {
LogManager.shared.log(category: .general, message: "App will terminate (\(ExitDiagnostics.memoryDescription()))")
#if !targetEnvironment(macCatalyst)
LiveActivityManager.shared.endOnTerminate()
#endif
Expand Down
89 changes: 89 additions & 0 deletions LoopFollow/Helpers/ExitDiagnostics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// LoopFollow
// ExitDiagnostics.swift

import Foundation
import MetricKit
import os

/// Logs MetricKit app exit counts and crash diagnostics. iOS delivers these
/// at most once a day, so a log shared the day after a termination shows why
/// the process ended.
final class ExitDiagnostics: NSObject, MXMetricManagerSubscriber {
static let shared = ExitDiagnostics()

func start() {
MXMetricManager.shared.add(self)
}

func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
guard let exits = payload.applicationExitMetrics else { continue }
let range = "\(payload.timeStampBegin) – \(payload.timeStampEnd)"

let bg = exits.backgroundExitData
let background = Self.describe([
("normal", bg.cumulativeNormalAppExitCount),
("memoryLimit", bg.cumulativeMemoryResourceLimitExitCount),
("memoryPressure", bg.cumulativeMemoryPressureExitCount),
("cpuLimit", bg.cumulativeCPUResourceLimitExitCount),
("badAccess", bg.cumulativeBadAccessExitCount),
("illegalInstruction", bg.cumulativeIllegalInstructionExitCount),
("abnormal", bg.cumulativeAbnormalExitCount),
("watchdog", bg.cumulativeAppWatchdogExitCount),
("lockedFile", bg.cumulativeSuspendedWithLockedFileExitCount),
("taskAssertionTimeout", bg.cumulativeBackgroundTaskAssertionTimeoutExitCount),
])

let fg = exits.foregroundExitData
let foreground = Self.describe([
("normal", fg.cumulativeNormalAppExitCount),
("memoryLimit", fg.cumulativeMemoryResourceLimitExitCount),
("badAccess", fg.cumulativeBadAccessExitCount),
("illegalInstruction", fg.cumulativeIllegalInstructionExitCount),
("abnormal", fg.cumulativeAbnormalExitCount),
("watchdog", fg.cumulativeAppWatchdogExitCount),
])

LogManager.shared.log(category: .general, message: "App exits \(range): background [\(background)], foreground [\(foreground)]")
}
}

func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
for crash in payload.crashDiagnostics ?? [] {
let signal = crash.signal.map { "\($0)" } ?? "-"
let exceptionType = crash.exceptionType.map { "\($0)" } ?? "-"
let exceptionCode = crash.exceptionCode.map { "\($0)" } ?? "-"
let reason = crash.terminationReason ?? "-"
LogManager.shared.log(
category: .general,
message: "Crash reported for \(payload.timeStampBegin) – \(payload.timeStampEnd): version=\(crash.applicationVersion), signal=\(signal), exceptionType=\(exceptionType), exceptionCode=\(exceptionCode), terminationReason=\(reason)"
)
}
}
}

/// Current memory footprint and the headroom left before iOS terminates the
/// process for exceeding its memory limit.
static func memoryDescription() -> String {
var info = task_vm_info_data_t()
var count = mach_msg_type_number_t(MemoryLayout<task_vm_info_data_t>.size / MemoryLayout<natural_t>.size)
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
let footprint = result == KERN_SUCCESS ? "\(info.phys_footprint / 1_048_576) MB" : "unknown"
#if targetEnvironment(macCatalyst)
return "memoryFootprint=\(footprint)"
#else
let available = os_proc_available_memory() / 1_048_576
return "memoryFootprint=\(footprint), memoryAvailable=\(available) MB"
#endif
}

private static func describe(_ counts: [(String, Int)]) -> String {
let nonZero = counts.filter { $0.1 > 0 }
return nonZero.isEmpty ? "none" : nonZero.map { "\($0.0)=\($0.1)" }.joined(separator: ", ")
}
}
2 changes: 1 addition & 1 deletion LoopFollow/ViewControllers/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ class MainViewController: UIViewController, UNUserNotificationCenterDelegate {
@objc func appMovedToBackground() {
LogManager.shared.log(
category: .general,
message: "App moved to background (refreshType=\(Storage.shared.backgroundRefreshType.value.rawValue), lowPowerMode=\(ProcessInfo.processInfo.isLowPowerModeEnabled), backgroundRefreshStatus=\(Self.describe(UIApplication.shared.backgroundRefreshStatus)))"
message: "App moved to background (refreshType=\(Storage.shared.backgroundRefreshType.value.rawValue), lowPowerMode=\(ProcessInfo.processInfo.isLowPowerModeEnabled), backgroundRefreshStatus=\(Self.describe(UIApplication.shared.backgroundRefreshStatus)), \(ExitDiagnostics.memoryDescription()))"
)

// Allow screen to turn off
Expand Down