Skip to content

Commit 68e9673

Browse files
committed
RUM-12420 Add KSCrash report minifier
1 parent f1ecbe0 commit 68e9673

File tree

4 files changed

+413
-2
lines changed

4 files changed

+413
-2
lines changed

Datadog/Datadog.xcodeproj/project.pbxproj

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,10 @@
12441244
D2268ADC2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268ADA2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift */; };
12451245
D2268ADE2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */; };
12461246
D2268ADF2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */; };
1247+
D2268AE12EB4F58700875E44 /* DatadogMinifyFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE02EB4F58700875E44 /* DatadogMinifyFilter.swift */; };
1248+
D2268AE22EB4F58700875E44 /* DatadogMinifyFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE02EB4F58700875E44 /* DatadogMinifyFilter.swift */; };
1249+
D2268AE42EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE32EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift */; };
1250+
D2268AE52EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2268AE32EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift */; };
12471251
D22743D829DEB8B4001A7EF9 /* VitalInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FC3C3B2653A97700DEED9E /* VitalInfoTests.swift */; };
12481252
D22743D929DEB8B4001A7EF9 /* VitalInfoSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2EF44E2694FA14008A7DAE /* VitalInfoSamplerTests.swift */; };
12491253
D22743DA29DEB8B4001A7EF9 /* VitalMemoryReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BBBCBB265E71D100943419 /* VitalMemoryReaderTests.swift */; };
@@ -3326,6 +3330,8 @@
33263330
D2268AD72EB4DAC400875E44 /* AnyCrashReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyCrashReport.swift; sourceTree = "<group>"; };
33273331
D2268ADA2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashFieldDictionaryTests.swift; sourceTree = "<group>"; };
33283332
D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogTypeSafeFilterTests.swift; sourceTree = "<group>"; };
3333+
D2268AE02EB4F58700875E44 /* DatadogMinifyFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogMinifyFilter.swift; sourceTree = "<group>"; };
3334+
D2268AE32EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatadogMinifyFilterTests.swift; sourceTree = "<group>"; };
33293335
D22789352D64A0D3007E9DB0 /* UploadQualityMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadQualityMetric.swift; sourceTree = "<group>"; };
33303336
D227A0A32C7622EA00C83324 /* BenchmarkProfiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BenchmarkProfiler.swift; sourceTree = "<group>"; };
33313337
D22C1F5B271484B400922024 /* LogEventMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEventMapper.swift; sourceTree = "<group>"; };
@@ -6538,8 +6544,9 @@
65386544
children = (
65396545
D22689C22EB12D3D00875E44 /* KSCrashPlugin.swift */,
65406546
D2268AD42EB4DA5800875E44 /* CrashFieldDictionary.swift */,
6541-
D2268AD12EB4DA4100875E44 /* DatadogTypeSafeFilter.swift */,
65426547
D2268AD72EB4DAC400875E44 /* AnyCrashReport.swift */,
6548+
D2268AD12EB4DA4100875E44 /* DatadogTypeSafeFilter.swift */,
6549+
D2268AE02EB4F58700875E44 /* DatadogMinifyFilter.swift */,
65436550
);
65446551
path = KSCrashIntegration;
65456552
sourceTree = "<group>";
@@ -6550,6 +6557,7 @@
65506557
D22689C62EB2151F00875E44 /* KSCrashPluginTests.swift */,
65516558
D2268ADA2EB4E69C00875E44 /* CrashFieldDictionaryTests.swift */,
65526559
D2268ADD2EB4EC6700875E44 /* DatadogTypeSafeFilterTests.swift */,
6560+
D2268AE32EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift */,
65536561
);
65546562
path = KSCrashIntegration;
65556563
sourceTree = "<group>";
@@ -9398,6 +9406,7 @@
93989406
D214DA8129DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */,
93999407
6167E7032B81F2EB00C3CA2D /* BacktraceReporter.swift in Sources */,
94009408
612556B0268C8D31002BCE74 /* CrashReportInfo.swift in Sources */,
9409+
D2268AE12EB4F58700875E44 /* DatadogMinifyFilter.swift in Sources */,
94019410
D214DA8B29DF2D6A004D0AE8 /* CrashContextProvider.swift in Sources */,
94029411
615CC40C2694A56D0005F08C /* SwiftExtensions.swift in Sources */,
94039412
D2268AD52EB4DA5800875E44 /* CrashFieldDictionary.swift in Sources */,
@@ -9420,6 +9429,7 @@
94209429
61E95D882695C00200EA3115 /* DDCrashReportExporterTests.swift in Sources */,
94219430
61B7886225C180CB002675B5 /* CrashReportingPluginTests.swift in Sources */,
94229431
615CC4132695957C0005F08C /* CrashReportInfoTests.swift in Sources */,
9432+
D2268AE42EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift in Sources */,
94239433
);
94249434
runOnlyForDeploymentPostprocessing = 0;
94259435
};
@@ -10612,6 +10622,7 @@
1061210622
D214DA8229DF2D5E004D0AE8 /* CrashReportingPlugin.swift in Sources */,
1061310623
6167E7042B81F2EB00C3CA2D /* BacktraceReporter.swift in Sources */,
1061410624
D2CB6FC427C5348200A62B57 /* CrashReportInfo.swift in Sources */,
10625+
D2268AE22EB4F58700875E44 /* DatadogMinifyFilter.swift in Sources */,
1061510626
D214DA8E29DF2D6B004D0AE8 /* CrashContextProvider.swift in Sources */,
1061610627
D2CB6FC527C5348200A62B57 /* SwiftExtensions.swift in Sources */,
1061710628
D2268AD62EB4DA5800875E44 /* CrashFieldDictionary.swift in Sources */,
@@ -10634,6 +10645,7 @@
1063410645
D2CB6FDE27C5352300A62B57 /* DDCrashReportExporterTests.swift in Sources */,
1063510646
D2CB6FE027C5352300A62B57 /* CrashReportingPluginTests.swift in Sources */,
1063610647
D2CB6FE127C5352300A62B57 /* CrashReportInfoTests.swift in Sources */,
10648+
D2268AE52EB4F5CA00875E44 /* DatadogMinifyFilterTests.swift in Sources */,
1063710649
);
1063810650
runOnlyForDeploymentPostprocessing = 0;
1063910651
};
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2019-Present Datadog, Inc.
5+
*/
6+
7+
import Foundation
8+
// swiftlint:disable duplicate_imports
9+
#if COCOAPODS
10+
import KSCrash
11+
#elseif swift(>=6.0)
12+
internal import KSCrashRecording
13+
#else
14+
@_implementationOnly import KSCrashRecording
15+
#endif
16+
// swiftlint:enable duplicate_imports
17+
18+
internal final class DatadogMinifyFilter: NSObject, CrashReportFilter {
19+
struct Constants {
20+
/// The maximum number of stack frames in each stack trace.
21+
/// When stack trace exceeds this limit, it will be reduced by dropping less important frames.
22+
static let maxNumberOfStackFrames = 200
23+
}
24+
25+
/// The maximum number of stack frames in each stack trace.
26+
let stackFramesLimit: Int
27+
28+
required init(stackFramesLimit: Int = Constants.maxNumberOfStackFrames) {
29+
self.stackFramesLimit = stackFramesLimit
30+
super.init()
31+
}
32+
33+
/// Filter the specified reports.
34+
///
35+
/// - Parameters:
36+
/// - reports: The reports to process.
37+
/// - onCompletion: Block to call when processing is complete.
38+
func filterReports(
39+
_ reports: [KSCrashRecording.CrashReport],
40+
onCompletion: (([KSCrashRecording.CrashReport]?, (Error)?) -> Void)?
41+
) {
42+
do {
43+
let reports = try reports.map { report in
44+
// Validate and extract the type-safe report dictionary
45+
guard var dict = report.untypedValue as? CrashFieldDictionary else {
46+
throw CrashReportException(description: "KSCrash report untypedValue is not a CrashDictionary")
47+
}
48+
49+
return try AnyCrashReport(minify(report: dict))
50+
}
51+
52+
onCompletion?(reports, nil)
53+
} catch {
54+
onCompletion?(reports, error)
55+
}
56+
}
57+
58+
/// Minifies a crash report by removing unused binary images.
59+
///
60+
/// This method reduces the crash report size by filtering out binary images (libraries/frameworks)
61+
/// that don't appear in any thread's backtrace. Only binary images that are actually referenced
62+
/// in stack frames are kept.
63+
///
64+
/// ## Algorithm
65+
///
66+
/// 1. Collects all `object_addr` values from all thread backtraces into a set
67+
/// 2. Filters `binary_images` to keep only those whose `image_addr` appears in the set
68+
///
69+
/// - Parameter report: The crash report to minify
70+
/// - Returns: A minified crash report with only referenced binary images
71+
/// - Throws: `CrashReportException` if report structure is invalid
72+
func minify(report: CrashFieldDictionary) throws -> CrashFieldDictionary {
73+
var minifiedReport = report
74+
var objectAddresses: Set<Int64> = []
75+
76+
if var threads: [CrashFieldDictionary] = try report.valueIfPresent(forKey: .crash, .threads) {
77+
threads = try threads.map { try self.minify(thread: $0, objectAddresses: &objectAddresses) }
78+
minifiedReport.setValue(forKey: .crash, .threads, value: threads)
79+
}
80+
81+
if var threads: [CrashFieldDictionary] = try report.valueIfPresent(forKey: .recrashReport, .crash, .threads) {
82+
threads = try threads.map { try self.minify(thread: $0, objectAddresses: &objectAddresses) }
83+
minifiedReport.setValue(forKey: .recrashReport, .crash, .threads, value: threads)
84+
}
85+
86+
// Filter binary images to keep only those referenced in backtraces
87+
let binaryImages: [CrashFieldDictionary] = try report.value(forKey: .binaryImages)
88+
minifiedReport[.binaryImages] = try binaryImages.filter { image in
89+
try image.valueIfPresent(forKey: .imageAddress).map { objectAddresses.contains($0) } ?? false
90+
}
91+
92+
return minifiedReport
93+
}
94+
95+
private func minify(thread: CrashFieldDictionary, objectAddresses: inout Set<Int64>) throws -> CrashFieldDictionary {
96+
guard var backtrace: [CrashFieldDictionary] = try thread.valueIfPresent(forKey: .backtrace, .contents) else {
97+
return thread
98+
}
99+
100+
let truncated = limit(backtrace: &backtrace)
101+
try objectAddresses.formUnion(backtrace.compactMap { try $0.valueIfPresent(forKey: .objectAddr) })
102+
103+
guard truncated else {
104+
return thread
105+
}
106+
107+
var thread = thread
108+
thread.setValue(forKey: .backtrace, .contents, value: backtrace)
109+
thread.setValue(forKey: .backtrace, .truncated, value: true)
110+
return thread
111+
}
112+
113+
/// Removes less important stack frames to ensure that their count is equal or below `stackFramesLimit`.
114+
/// Frames are removed at the middle of stack trace, which preserves the most important upper and bottom frames.
115+
private func limit(backtrace frames: inout [CrashFieldDictionary]) -> Bool {
116+
guard frames.count > stackFramesLimit else {
117+
return false
118+
}
119+
120+
let toRemove = frames.count - stackFramesLimit
121+
let middleFrameIndex = frames.count / 2
122+
let lowerBound = middleFrameIndex - toRemove / 2
123+
let upperBound = lowerBound + toRemove
124+
frames.removeSubrange(lowerBound..<upperBound)
125+
126+
return true
127+
}
128+
}

DatadogCrashReporting/Sources/KSCrashIntegration/KSCrashPlugin.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ internal class KSCrashPlugin: NSObject, CrashReportingPlugin {
2929
try kscrash.install(with: .datadog())
3030
kscrash.reportStore?.sink = CrashReportFilterPipeline(
3131
filters: [
32-
DatadogTypeSafeFilter()
32+
DatadogTypeSafeFilter(),
33+
DatadogMinifyFilter()
3334
]
3435
)
3536

0 commit comments

Comments
 (0)