Skip to content

Commit

Permalink
Add attachment generator for MetricKit metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
NickEntin committed Dec 4, 2023
1 parent 58ac871 commit d659f04
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 3 deletions.
10 changes: 7 additions & 3 deletions Aardvark.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -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 */; };
Expand All @@ -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 */; };
Expand Down Expand Up @@ -168,6 +169,7 @@
3D81BC5725C54A0800E61A49 /* LogStoreAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStoreAttachmentGenerator.swift; sourceTree = "<group>"; };
3D850496215EF20800B3957C /* ARKExceptionLogging_Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARKExceptionLogging_Testing.h; sourceTree = "<group>"; };
3D850498215EF6F500B3957C /* ARKExceptionLoggingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ARKExceptionLoggingTests.m; sourceTree = "<group>"; };
3D8FEC0C2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsAttachmentGenerator.swift; sourceTree = "<group>"; };
3D90DEB720AA9B19006D4924 /* ARKEmailBugReportConfiguration_Protected.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ARKEmailBugReportConfiguration_Protected.h; sourceTree = "<group>"; };
3D9F0A14255BC728000E63D7 /* ARKEmailAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARKEmailAttachment.swift; sourceTree = "<group>"; };
3DA5BF30255657C100B6D148 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Resources/en.lproj/Localizable.strings; sourceTree = "<group>"; };
Expand All @@ -179,8 +181,8 @@
3DA5BF572556617000B6D148 /* Aardvark+EmailBugReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Aardvark+EmailBugReporting.swift"; sourceTree = "<group>"; };
3DA5BF5F2556640100B6D148 /* ARKBugReportAttachmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARKBugReportAttachmentTests.swift; sourceTree = "<group>"; };
3DF0CB53261C6F4600B02A7C /* FileSystemAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemAttachmentGenerator.swift; sourceTree = "<group>"; };
3DFD7B5026F5519D000CE4B8 /* FileSystemAttachmentGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemAttachmentGeneratorTests.swift; sourceTree = "<group>"; };
3DFD25DA26F3FD82000CE4B8 /* UserDefaultsAttachmentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsAttachmentGenerator.swift; sourceTree = "<group>"; };
3DFD7B5026F5519D000CE4B8 /* FileSystemAttachmentGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemAttachmentGeneratorTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
4551A3081BDAF93A00F216D0 /* ARKScreenshotLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ARKScreenshotLogging.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -471,6 +473,7 @@
3D81BC4325C50A9800E61A49 /* ViewHierarchyAttachmentGenerator.swift */,
3D81BC5725C54A0800E61A49 /* LogStoreAttachmentGenerator.swift */,
3DF0CB53261C6F4600B02A7C /* FileSystemAttachmentGenerator.swift */,
3D8FEC0C2B0DEA13005DA3EB /* MetricsAttachmentGenerator.swift */,
3DFD25DA26F3FD82000CE4B8 /* UserDefaultsAttachmentGenerator.swift */,
);
path = BugReporting;
Expand Down Expand Up @@ -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 */,
Expand Down
287 changes: 287 additions & 0 deletions Sources/Aardvark/BugReporting/MetricsAttachmentGenerator.swift
Original file line number Diff line number Diff line change
@@ -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<Metric> = 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<Metric> = 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<MetricsAttachmentGenerator.Metric>) -> 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<Unit>(for histogram: MXHistogram<Unit>, 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<Unit>
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
}

}

0 comments on commit d659f04

Please sign in to comment.