Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add attachment generator for MetricKit metrics #147

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
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
}

}
Loading