From d659f04f2ba63c7bb076de5c2cffb031ddcb7a5a Mon Sep 17 00:00:00 2001 From: Nicholas Entin Date: Sat, 25 Nov 2023 01:26:32 -0800 Subject: [PATCH] Add attachment generator for MetricKit metrics --- Aardvark.xcodeproj/project.pbxproj | 10 +- .../MetricsAttachmentGenerator.swift | 287 ++++++++++++++++++ 2 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 Sources/Aardvark/BugReporting/MetricsAttachmentGenerator.swift diff --git a/Aardvark.xcodeproj/project.pbxproj b/Aardvark.xcodeproj/project.pbxproj index 4f9c735..48fb2a4 100644 --- a/Aardvark.xcodeproj/project.pbxproj +++ b/Aardvark.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -36,6 +36,7 @@ 3D81BC5825C54A0800E61A49 /* LogStoreAttachmentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D81BC5725C54A0800E61A49 /* LogStoreAttachmentGenerator.swift */; }; 3D850497215EF21100B3957C /* ARKExceptionLogging_Testing.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D850496215EF20800B3957C /* ARKExceptionLogging_Testing.h */; }; 3D850499215EF6F500B3957C /* ARKExceptionLoggingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D850498215EF6F500B3957C /* ARKExceptionLoggingTests.m */; }; + 3D8FEC0D2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D8FEC0C2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift */; }; 3D9F0A15255BC728000E63D7 /* ARKEmailAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9F0A14255BC728000E63D7 /* ARKEmailAttachment.swift */; }; 3DA5BF31255657C100B6D148 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3DA5BF2F255657C100B6D148 /* Localizable.strings */; }; 3DA5BF402556602100B6D148 /* AardvarkMailUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DA5BF372556602000B6D148 /* AardvarkMailUI.framework */; }; @@ -54,8 +55,8 @@ 3DA743201F9D4EE500ADB183 /* ARKExceptionLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D15E02D1F9D38B1001DE13A /* ARKExceptionLogging.m */; }; 3DD020DF2556502E00E6400A /* ARKDefaultLogFormatterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EAAB38A319E2929C00161A54 /* ARKDefaultLogFormatterTests.m */; }; 3DF0CB54261C6F4600B02A7C /* FileSystemAttachmentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF0CB53261C6F4600B02A7C /* FileSystemAttachmentGenerator.swift */; }; - 3DFD7B5226F551C8000CE4B8 /* FileSystemAttachmentGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFD7B5026F5519D000CE4B8 /* FileSystemAttachmentGeneratorTests.swift */; }; 3DFD25DB26F3FD82000CE4B8 /* UserDefaultsAttachmentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFD25DA26F3FD82000CE4B8 /* UserDefaultsAttachmentGenerator.swift */; }; + 3DFD7B5226F551C8000CE4B8 /* FileSystemAttachmentGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFD7B5026F5519D000CE4B8 /* FileSystemAttachmentGeneratorTests.swift */; }; 4551A2D91BDAD10E00F216D0 /* Aardvark.h in Headers */ = {isa = PBXBuildFile; fileRef = EAD1442419E073FB0065A1FF /* Aardvark.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4551A30A1BDAF93A00F216D0 /* ARKScreenshotLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 4551A3071BDAF93A00F216D0 /* ARKScreenshotLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; EA3C1D961D934A210048C4CD /* CoreAardvark.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAF2FEA01D47172400931663 /* CoreAardvark.framework */; }; @@ -168,6 +169,7 @@ 3D81BC5725C54A0800E61A49 /* LogStoreAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStoreAttachmentGenerator.swift; sourceTree = ""; }; 3D850496215EF20800B3957C /* ARKExceptionLogging_Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARKExceptionLogging_Testing.h; sourceTree = ""; }; 3D850498215EF6F500B3957C /* ARKExceptionLoggingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARKExceptionLoggingTests.m; sourceTree = ""; }; + 3D8FEC0C2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsAttachmentGenerator.swift; sourceTree = ""; }; 3D90DEB720AA9B19006D4924 /* ARKEmailBugReportConfiguration_Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARKEmailBugReportConfiguration_Protected.h; sourceTree = ""; }; 3D9F0A14255BC728000E63D7 /* ARKEmailAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARKEmailAttachment.swift; sourceTree = ""; }; 3DA5BF30255657C100B6D148 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Resources/en.lproj/Localizable.strings; sourceTree = ""; }; @@ -179,8 +181,8 @@ 3DA5BF572556617000B6D148 /* Aardvark+EmailBugReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Aardvark+EmailBugReporting.swift"; sourceTree = ""; }; 3DA5BF5F2556640100B6D148 /* ARKBugReportAttachmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARKBugReportAttachmentTests.swift; sourceTree = ""; }; 3DF0CB53261C6F4600B02A7C /* FileSystemAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemAttachmentGenerator.swift; sourceTree = ""; }; - 3DFD7B5026F5519D000CE4B8 /* FileSystemAttachmentGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemAttachmentGeneratorTests.swift; sourceTree = ""; }; 3DFD25DA26F3FD82000CE4B8 /* UserDefaultsAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsAttachmentGenerator.swift; sourceTree = ""; }; + 3DFD7B5026F5519D000CE4B8 /* FileSystemAttachmentGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemAttachmentGeneratorTests.swift; sourceTree = ""; }; 4551A2C21BDACF9000F216D0 /* Aardvark.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Aardvark.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4551A3071BDAF93A00F216D0 /* ARKScreenshotLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ARKScreenshotLogging.h; sourceTree = ""; }; 4551A3081BDAF93A00F216D0 /* ARKScreenshotLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARKScreenshotLogging.m; sourceTree = ""; }; @@ -471,6 +473,7 @@ 3D81BC4325C50A9800E61A49 /* ViewHierarchyAttachmentGenerator.swift */, 3D81BC5725C54A0800E61A49 /* LogStoreAttachmentGenerator.swift */, 3DF0CB53261C6F4600B02A7C /* FileSystemAttachmentGenerator.swift */, + 3D8FEC0C2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift */, 3DFD25DA26F3FD82000CE4B8 /* UserDefaultsAttachmentGenerator.swift */, ); path = BugReporting; @@ -952,6 +955,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D8FEC0D2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift in Sources */, EA98B9531D4BF43400B3A390 /* ARKScreenshotLogging.m in Sources */, 3D81BC5825C54A0800E61A49 /* LogStoreAttachmentGenerator.swift in Sources */, 3D9F0A15255BC728000E63D7 /* ARKEmailAttachment.swift in Sources */, diff --git a/Sources/Aardvark/BugReporting/MetricsAttachmentGenerator.swift b/Sources/Aardvark/BugReporting/MetricsAttachmentGenerator.swift new file mode 100644 index 0000000..e227f92 --- /dev/null +++ b/Sources/Aardvark/BugReporting/MetricsAttachmentGenerator.swift @@ -0,0 +1,287 @@ +// +// Copyright 2023 Block, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//    http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import MetricKit + +@available(iOS 13, *) +@objc(ARKMetricsAttachmentGenerator) +public final class MetricsAttachmentGenerator: NSObject { + + // MARK: - Public Static Methods + + public static func latestMetricsAttachment(metrics: Set = Set(Metric.allCases)) -> ARKBugReportAttachment? { + guard let metricsPayload = MXMetricManager.shared.pastPayloads + .sorted(by: { $0.timeStampEnd < $1.timeStampEnd }) + .last + else { + return nil + } + + let dateFormatter = ISO8601DateFormatter() + + return ARKBugReportAttachment( + fileName: "Application Metrics (\(dateFormatter.string(from: metricsPayload.timeStampBegin)) - \(dateFormatter.string(from: metricsPayload.timeStampEnd))).txt", + data: Data(metricsPayload.attachmentDescription(for: metrics).utf8), + dataMIMEType: "text/plain" + ) + } + + public static func allMetricsAttachments(metrics: Set = Set(Metric.allCases)) -> [ARKBugReportAttachment] { + let dateFormatter = ISO8601DateFormatter() + + return MXMetricManager.shared.pastPayloads.map { metricsPayload in + return ARKBugReportAttachment( + fileName: "Application Metrics (\(dateFormatter.string(from: metricsPayload.timeStampBegin)) - \(dateFormatter.string(from: metricsPayload.timeStampEnd))).txt", + data: Data(metricsPayload.attachmentDescription(for: metrics).utf8), + dataMIMEType: "text/plain" + ) + } + } + + // MARK: - Public Types + + public enum Metric: CaseIterable { + + // Metrics for debugging performance + + case applicationExit + case applicationTime + case memoryUsage + + // Metrics for debugging responsiveness + + case applicationLaunch + case applicationResponsiveness + case animationResponsiveness + + // Metrics for debugging battery usage + + case cpuUsage + case gpuUsage + case displayUsage + case locationActivity + + // Metrics for network data + + case networkActivity + case cellularConditions + + // Metrics for disk access + + case diskIO + + } + +} + +// MARK: - + +@available(iOS 13, *) +extension MXMetricPayload { + + fileprivate func attachmentDescription(for includedMetrics: Set) -> String { + var descriptions: [String] = [] + + let dateFormatter = ISO8601DateFormatter() + descriptions.append( + """ + Metrics for \(dateFormatter.string(from: timeStampBegin)) to \(dateFormatter.string(from: timeStampEnd)) + + App Version: \(latestApplicationVersion)\(includesMultipleApplicationVersions ? " and older versions" : "") + """ + ) + + let measurementFormattter = MeasurementFormatter() + measurementFormattter.unitStyle = .short + + if #available(iOS 14, *), let metrics = applicationExitMetrics, includedMetrics.contains(.applicationExit) { + descriptions.append( + """ + # of Foreground Exits by Reason: + Normal: \(metrics.foregroundExitData.cumulativeNormalAppExitCount) + Abnormal: \(metrics.foregroundExitData.cumulativeAbnormalExitCount) + App Watchdog: \(metrics.foregroundExitData.cumulativeAppWatchdogExitCount) + Memory Limit: \(metrics.foregroundExitData.cumulativeMemoryResourceLimitExitCount) + Bad Access: \(metrics.foregroundExitData.cumulativeBadAccessExitCount) + Illegal Instruction: \(metrics.foregroundExitData.cumulativeIllegalInstructionExitCount) + + # of Background Exits by Reason: + Normal: \(metrics.backgroundExitData.cumulativeNormalAppExitCount) + Abnormal: \(metrics.backgroundExitData.cumulativeAbnormalExitCount) + App Watchdog: \(metrics.backgroundExitData.cumulativeAppWatchdogExitCount) + CPU Limit: \(metrics.backgroundExitData.cumulativeCPUResourceLimitExitCount) + Memory Limit: \(metrics.backgroundExitData.cumulativeMemoryResourceLimitExitCount) + Memory Pressure: \(metrics.backgroundExitData.cumulativeMemoryPressureExitCount) + Suspended w/ Locked File: \(metrics.backgroundExitData.cumulativeSuspendedWithLockedFileExitCount) + Bad Access: \(metrics.backgroundExitData.cumulativeBadAccessExitCount) + Illegal Instruction: \(metrics.backgroundExitData.cumulativeIllegalInstructionExitCount) + Background Task Timeout: \(metrics.backgroundExitData.cumulativeBackgroundTaskAssertionTimeoutExitCount) + """ + ) + } + + if let metrics = applicationTimeMetrics, includedMetrics.contains(.applicationTime) { + descriptions.append( + """ + Cumulative Time by Application State: + Foreground: \(measurementFormattter.string(from: metrics.cumulativeForegroundTime)) + Background: \(measurementFormattter.string(from: metrics.cumulativeBackgroundTime)) + Background Audio: \(measurementFormattter.string(from: metrics.cumulativeBackgroundAudioTime)) + Background Location: \(measurementFormattter.string(from: metrics.cumulativeBackgroundLocationTime)) + """ + ) + } + + if let metrics = memoryMetrics, includedMetrics.contains(.memoryUsage) { + descriptions.append( + """ + Average Suspended Memory: \(measurementFormattter.string(from: metrics.averageSuspendedMemory.averageMeasurement)) + Peak Memory Usage: \(measurementFormattter.string(from: metrics.peakMemoryUsage)) + """ + ) + } + + if let metrics = applicationLaunchMetrics, includedMetrics.contains(.applicationLaunch) { + if #available(iOS 15.2, *) { + descriptions.append(histogramDescription(for: metrics.histogrammedOptimizedTimeToFirstDraw, named: "Optimized Time to First Draw")) + } + descriptions.append(histogramDescription(for: metrics.histogrammedTimeToFirstDraw, named: "Time to First Draw")) + descriptions.append(histogramDescription(for: metrics.histogrammedApplicationResumeTime, named: "Application Resume Time")) + if #available(iOS 16.0, *) { + descriptions.append(histogramDescription(for: metrics.histogrammedExtendedLaunch, named: "Extended Launch Time")) + } + } + + if let metrics = applicationResponsivenessMetrics, includedMetrics.contains(.applicationResponsiveness) { + descriptions.append(histogramDescription(for: metrics.histogrammedApplicationHangTime, named: "Application Hang Time")) + } + + if #available(iOS 14, *), let metrics = animationMetrics, includedMetrics.contains(.animationResponsiveness) { + descriptions.append( + """ + Scroll Hitch Time Ratio: \(measurementFormattter.string(from: metrics.scrollHitchTimeRatio)) + """ + ) + } + + if let metrics = cpuMetrics, includedMetrics.contains(.cpuUsage) { + descriptions.append( + """ + Cumulative CPU Time: \(measurementFormattter.string(from: metrics.cumulativeCPUTime)) + """ + ) + } + + if let metrics = gpuMetrics, includedMetrics.contains(.gpuUsage) { + descriptions.append( + """ + Cumulative GPU Time: \(measurementFormattter.string(from: metrics.cumulativeGPUTime)) + """ + ) + } + + if let averagePixelLuminance = displayMetrics?.averagePixelLuminance, includedMetrics.contains(.displayUsage) { + descriptions.append( + """ + Average Pixel Luminance: \(measurementFormattter.string(from: averagePixelLuminance.averageMeasurement)) + """ + ) + } + + if let metrics = locationActivityMetrics, includedMetrics.contains(.locationActivity) { + descriptions.append( + """ + Cumulative Time by Accuracy: + Best for Navigation: \(measurementFormattter.string(from: metrics.cumulativeBestAccuracyForNavigationTime)) + Best: \(measurementFormattter.string(from: metrics.cumulativeBestAccuracyTime)) + Nearest 10 Meters: \(measurementFormattter.string(from: metrics.cumulativeNearestTenMetersAccuracyTime)) + 100 Meters: \(measurementFormattter.string(from: metrics.cumulativeHundredMetersAccuracyTime)) + 1 Kilometer: \(measurementFormattter.string(from: metrics.cumulativeKilometerAccuracyTime)) + 3 Kilometer: \(measurementFormattter.string(from: metrics.cumulativeThreeKilometersAccuracyTime)) + """ + ) + } + + if let metrics = networkTransferMetrics, includedMetrics.contains(.networkActivity) { + descriptions.append( + """ + Cumulative Cellular Down: \(measurementFormattter.string(from: metrics.cumulativeCellularDownload)) + Cumulative Cellular Up: \(measurementFormattter.string(from: metrics.cumulativeCellularUpload)) + Cumulative WiFi Down: \(measurementFormattter.string(from: metrics.cumulativeWifiDownload)) + Cumulative WiFi Up: \(measurementFormattter.string(from: metrics.cumulativeWifiUpload)) + """ + ) + } + + if let metrics = cellularConditionMetrics, includedMetrics.contains(.cellularConditions) { + descriptions.append(histogramDescription(for: metrics.histogrammedCellularConditionTime, named: "Cellular Condition Time")) + } + + if let metrics = diskIOMetrics, includedMetrics.contains(.diskIO) { + descriptions.append( + """ + Cumulative Logical Writes: \(measurementFormattter.string(from: metrics.cumulativeLogicalWrites)) + """ + ) + } + + return descriptions.joined(separator: "\n\n") + } + + private func histogramDescription(for histogram: MXHistogram, named name: String) -> String { + let valueFormatter = MeasurementFormatter() + + let barsFormatter = NumberFormatter() + barsFormatter.maximumFractionDigits = 2 + + var buckets: [(String, Int)] = [] + for bucket in histogram.bucketEnumerator { + let bucket = bucket as! MXHistogramBucket + if Unit.self == MXUnitSignalBars.self { + let bucketStartString = barsFormatter.string(from: NSNumber(value: bucket.bucketStart.value))! + let bucketEndString = barsFormatter.string(from: NSNumber(value: bucket.bucketEnd.value))! + buckets.append( + ( + "\(bucketStartString)\(bucket.bucketEnd == bucket.bucketStart ? "" : " - " + bucketEndString) bars", + bucket.bucketCount + ) + ) + } else { + buckets.append( + ( + "\(valueFormatter.string(from: bucket.bucketStart)) - \(valueFormatter.string(from: bucket.bucketEnd))", + bucket.bucketCount + ) + ) + } + } + + let longestBucketLabelLength = buckets.reduce(22, { max($0, $1.0.count) }) + + var description = "\(name):" + for bucket in buckets { + description.append("\n \(bucket.0)") + description.append(String(repeating: " ", count: longestBucketLabelLength + 4 - bucket.0.count)) + description.append("\(bucket.1)") + } + if buckets.isEmpty { + description.append("\n (no data available)") + } + return description + } + +}