// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/app/application_delegate/metric_kit_subscriber.h"
#import "base/apple/foundation_util.h"
#import "base/files/file_path.h"
#import "base/files/file_util.h"
#import "base/metrics/histogram_base.h"
#import "base/metrics/histogram_functions.h"
#import "base/numerics/safe_conversions.h"
#import "base/path_service.h"
#import "base/strings/sys_string_conversions.h"
#import "base/task/task_traits.h"
#import "base/task/thread_pool.h"
#import "base/version.h"
#import "components/crash/core/app/crashpad.h"
#import "components/crash/core/common/reporter_running_ios.h"
#import "components/previous_session_info/previous_session_info.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/browser/crash_report/model/features.h"
// The different causes of app exit as reported by MetricKit.
// This enum is used in UMA. Do not change the order.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum MetricKitExitReason {
kNormalAppExit = 0,
kAbnormalAppExit = 1,
kWatchdogExit = 2,
kCPUResourceLimitExit = 3,
kMemoryResourceLimitExit = 4,
kMemoryPressureExit = 5,
kSuspendedWithLockedFileExit = 6,
kBadAccessExit = 7,
kIllegalInstructionExit = 8,
kBackgroundTaskAssertionTimeoutExit = 9,
// Must be the last enum entries.
kMetricKitExitReasonMaxValue = kBackgroundTaskAssertionTimeoutExit,
kMetricKitExitReasonCount = kMetricKitExitReasonMaxValue + 1
};
namespace {
// Task identifier for tracking startup until the app becomes interactive.
NSString* const kMainLaunchTaskId = @"MainLaunchTask";
void ReportExitReason(base::HistogramBase* histogram,
MetricKitExitReason bucket,
NSUInteger count) {
if (!count) {
return;
}
histogram->AddCount(bucket, count);
}
void ReportLongDuration(const std::string& histogram_name,
NSMeasurement* measurement) {
if (!measurement) {
return;
}
double value =
[measurement measurementByConvertingToUnit:NSUnitDuration.seconds]
.doubleValue;
base::UmaHistogramCustomTimes(histogram_name, base::Seconds(value),
base::Seconds(1),
base::Seconds(86400 /* secs per day */), 50);
}
void ReportMemory(const std::string& histogram_name,
NSMeasurement* measurement) {
if (!measurement) {
return;
}
double value =
[measurement
measurementByConvertingToUnit:NSUnitInformationStorage.megabytes]
.doubleValue;
base::UmaHistogramMemoryLargeMB(histogram_name, value);
}
void SendDiagnostic(MXDiagnostic* diagnostic, const std::string& type) {
base::FilePath cache_dir_path;
if (!base::PathService::Get(base::DIR_CACHE, &cache_dir_path)) {
return;
}
// Deflate the payload.
NSError* error = nil;
NSData* payload = [diagnostic.JSONRepresentation
compressedDataUsingAlgorithm:NSDataCompressionAlgorithmZlib
error:&error];
if (!payload) {
return;
}
if (crash_reporter::IsCrashpadRunning()) {
base::span<const uint8_t> spanpayload = base::apple::NSDataToSpan(payload);
std::map<std::string, std::string> override_annotations = {
{"ver",
base::SysNSStringToUTF8(diagnostic.metaData.applicationBuildVersion)},
{"metrickit", "true"},
{"metrickit_type", type}};
PreviousSessionInfo* previous_session =
[PreviousSessionInfo sharedInstance];
for (NSString* key in previous_session.reportParameters.allKeys) {
override_annotations.insert(
{base::SysNSStringToUTF8(key),
base::SysNSStringToUTF8(previous_session.reportParameters[key])});
}
if (previous_session.breadcrumbs) {
override_annotations.insert(
{"breadcrumbs",
base::SysNSStringToUTF8(previous_session.breadcrumbs)});
}
crash_reporter::ProcessExternalDump("MetricKit", spanpayload,
override_annotations);
}
}
void ProcessDiagnosticPayloads(NSArray<MXDiagnosticPayload*>* payloads) {
for (MXDiagnosticPayload* payload in payloads) {
for (MXCrashDiagnostic* diagnostic in payload.crashDiagnostics) {
SendDiagnostic(diagnostic, "crash");
}
if (base::FeatureList::IsEnabled(kMetrickitNonCrashReport)) {
for (MXCPUExceptionDiagnostic* diagnostic in payload
.cpuExceptionDiagnostics) {
SendDiagnostic(diagnostic, "cpu-exception");
}
for (MXHangDiagnostic* diagnostic in payload.hangDiagnostics) {
SendDiagnostic(diagnostic, "hang");
}
for (MXDiskWriteExceptionDiagnostic* diagnostic in payload
.diskWriteExceptionDiagnostics) {
SendDiagnostic(diagnostic, "diskwrite-exception");
}
}
}
}
// Record MXPayload data even when the version is mismatched.
const char kHistogramPrefixIncludingMismatch[] =
"IOS.MetricKit.IncludingMismatch.";
const char kHistogramPrefix[] = "IOS.MetricKit.";
std::string HistogramPrefix(bool include_mismatch) {
return include_mismatch ? kHistogramPrefixIncludingMismatch
: kHistogramPrefix;
}
} // namespace
@implementation MetricKitSubscriber
+ (instancetype)sharedInstance {
static MetricKitSubscriber* instance = [[MetricKitSubscriber alloc] init];
return instance;
}
+ (void)createExtendedLaunchTask {
#if defined(__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
if (@available(iOS 16.0, *)) {
[MXMetricManager extendLaunchMeasurementForTaskID:kMainLaunchTaskId
error:nil];
}
#endif
}
+ (void)endExtendedLaunchTask {
#if defined(__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
if (@available(iOS 16.0, *)) {
[MXMetricManager finishExtendedLaunchMeasurementForTaskID:kMainLaunchTaskId
error:nil];
}
#endif
}
- (void)setEnabled:(BOOL)enable {
if (enable == _enabled) {
return;
}
_enabled = enable;
if (enable) {
[[MXMetricManager sharedManager] addSubscriber:self];
} else {
[[MXMetricManager sharedManager] removeSubscriber:self];
}
}
- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload*>*)payloads {
for (MXMetricPayload* payload : payloads) {
[self processPayload:payload];
}
}
- (void)logStartupDurationMXHistogram:(MXHistogram*)histogram
toUMAHistogram:(const std::string&)histogramUMAName {
if (!histogram || !histogram.totalBucketCount) {
return;
}
// It should take less than 1 minute to startup.
// Histogram is defined in millisecond granularity.
base::HistogramBase* histogramUMA = base::Histogram::FactoryTimeGet(
histogramUMAName, base::Milliseconds(1), base::Minutes(1), 50,
base::HistogramBase::kUmaTargetedHistogramFlag);
for (MXHistogramBucket* bucket in [histogram bucketEnumerator]) {
// MXHistogram structure is linear and the bucket size is not guaranteed to
// never change. As the granularity is small in the current iOS version,
// (10ms) they are reported using a representative value of the bucket.
// DCHECK on the size of the bucket to detect if the resolution decrease.
// Time based MXHistogram report their values using `UnitDuration` which has
// seconds as base unit. Hence, start and end are given in seconds.
double start =
[bucket.bucketStart
measurementByConvertingToUnit:NSUnitDuration.milliseconds]
.doubleValue;
double end = [bucket.bucketEnd
measurementByConvertingToUnit:NSUnitDuration.milliseconds]
.doubleValue;
// DCHECKS that resolution is less than 10ms.
// Note: Real paylods use 10ms resolution but the simulated payload in XCode
// uses 100ms resolution so it will trigger this DCHECK.
DCHECK_LE(end - start, 10);
double sample = (end + start) / 2;
histogramUMA->AddCount(
base::saturated_cast<base::HistogramBase::Sample>(sample),
bucket.bucketCount);
}
}
- (void)logForegroundExit:(MXForegroundExitData*)exitData
histogramPrefix:(const std::string&)prefix {
base::HistogramBase* histogramUMA = base::LinearHistogram::FactoryGet(
prefix + "ForegroundExitData", 1, kMetricKitExitReasonCount,
kMetricKitExitReasonCount + 1,
base::HistogramBase::kUmaTargetedHistogramFlag);
ReportExitReason(histogramUMA, kNormalAppExit,
exitData.cumulativeNormalAppExitCount);
ReportExitReason(histogramUMA, kAbnormalAppExit,
exitData.cumulativeAbnormalExitCount);
ReportExitReason(histogramUMA, kWatchdogExit,
exitData.cumulativeAppWatchdogExitCount);
ReportExitReason(histogramUMA, kMemoryResourceLimitExit,
exitData.cumulativeMemoryResourceLimitExitCount);
ReportExitReason(histogramUMA, kBadAccessExit,
exitData.cumulativeBadAccessExitCount);
ReportExitReason(histogramUMA, kIllegalInstructionExit,
exitData.cumulativeIllegalInstructionExitCount);
}
- (void)logBackgroundExit:(MXBackgroundExitData*)exitData
histogramPrefix:(const std::string&)prefix {
base::HistogramBase* histogramUMA = base::LinearHistogram::FactoryGet(
prefix + "BackgroundExitData", 1, kMetricKitExitReasonCount,
kMetricKitExitReasonCount + 1,
base::HistogramBase::kUmaTargetedHistogramFlag);
ReportExitReason(histogramUMA, kNormalAppExit,
exitData.cumulativeNormalAppExitCount);
ReportExitReason(histogramUMA, kAbnormalAppExit,
exitData.cumulativeAbnormalExitCount);
ReportExitReason(histogramUMA, kWatchdogExit,
exitData.cumulativeAppWatchdogExitCount);
ReportExitReason(histogramUMA, kCPUResourceLimitExit,
exitData.cumulativeCPUResourceLimitExitCount);
ReportExitReason(histogramUMA, kMemoryResourceLimitExit,
exitData.cumulativeMemoryResourceLimitExitCount);
ReportExitReason(histogramUMA, kMemoryPressureExit,
exitData.cumulativeMemoryPressureExitCount);
ReportExitReason(histogramUMA, kSuspendedWithLockedFileExit,
exitData.cumulativeSuspendedWithLockedFileExitCount);
ReportExitReason(histogramUMA, kBadAccessExit,
exitData.cumulativeBadAccessExitCount);
ReportExitReason(histogramUMA, kIllegalInstructionExit,
exitData.cumulativeIllegalInstructionExitCount);
ReportExitReason(histogramUMA, kBackgroundTaskAssertionTimeoutExit,
exitData.cumulativeBackgroundTaskAssertionTimeoutExitCount);
}
- (void)processPayload:(MXMetricPayload*)payload {
if (!payload.includesMultipleApplicationVersions &&
base::SysNSStringToUTF8(payload.metaData.applicationBuildVersion) ==
version_info::GetVersionNumber()) {
[self processPayload:payload withHistogramPrefix:HistogramPrefix(false)];
}
[self processPayload:payload withHistogramPrefix:HistogramPrefix(true)];
}
- (void)processPayload:(MXMetricPayload*)payload
withHistogramPrefix:(const std::string&)prefix {
ReportLongDuration(prefix + "ForegroundTimePerDay",
payload.applicationTimeMetrics.cumulativeForegroundTime);
ReportLongDuration(prefix + "BackgroundTimePerDay",
payload.applicationTimeMetrics.cumulativeBackgroundTime);
ReportLongDuration(prefix + "CPUTimePerDay",
payload.cpuMetrics.cumulativeCPUTime);
ReportMemory(prefix + "AverageSuspendedMemory",
payload.memoryMetrics.averageSuspendedMemory.averageMeasurement);
ReportMemory(prefix + "PeakMemoryUsage",
payload.memoryMetrics.peakMemoryUsage);
MXHistogram* histogrammedApplicationResumeTime =
payload.applicationLaunchMetrics.histogrammedApplicationResumeTime;
[self logStartupDurationMXHistogram:histogrammedApplicationResumeTime
toUMAHistogram:prefix + "ApplicationResumeTime"];
MXHistogram* histogrammedTimeToFirstDraw =
payload.applicationLaunchMetrics.histogrammedTimeToFirstDraw;
[self logStartupDurationMXHistogram:histogrammedTimeToFirstDraw
toUMAHistogram:prefix + "TimeToFirstDraw"];
#if defined(__IPHONE_16_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
if (@available(iOS 16.0, *)) {
MXHistogram* histogrammedOptimizedTimeToFirstDraw =
payload.applicationLaunchMetrics.histogrammedOptimizedTimeToFirstDraw;
[self logStartupDurationMXHistogram:histogrammedOptimizedTimeToFirstDraw
toUMAHistogram:prefix + "OptimizedTimeToFirstDraw"];
MXHistogram* histogrammedExtendedLaunch =
payload.applicationLaunchMetrics.histogrammedExtendedLaunch;
[self logStartupDurationMXHistogram:histogrammedExtendedLaunch
toUMAHistogram:prefix + "ExtendedLaunch"];
}
#endif
MXHistogram* histogrammedApplicationHangTime =
payload.applicationResponsivenessMetrics.histogrammedApplicationHangTime;
[self logStartupDurationMXHistogram:histogrammedApplicationHangTime
toUMAHistogram:prefix + "ApplicationHangTime"];
[self logForegroundExit:payload.applicationExitMetrics.foregroundExitData
histogramPrefix:prefix];
[self logBackgroundExit:payload.applicationExitMetrics.backgroundExitData
histogramPrefix:prefix];
}
- (void)didReceiveDiagnosticPayloads:(NSArray<MXDiagnosticPayload*>*)payloads {
base::ThreadPool::PostTask(
FROM_HERE,
{base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN,
base::ThreadPolicy::PREFER_BACKGROUND, base::MayBlock()},
base::BindOnce(ProcessDiagnosticPayloads, payloads));
}
@end