Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
2 changes: 0 additions & 2 deletions Backtrace.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ Pod::Spec.new do |s|
s.osx.public_header_files = ["Backtrace-macOS/**/*.h*"]
s.tvos.public_header_files = ["Backtrace-tvOS/**/*.h*"]

s.ios.dependency "Cassette", '1.0.0-beta5'
Copy link
Contributor

@vlussenburg vlussenburg May 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check if there's any superfluous #if os(iOS) || os(OSX) left in prod/test code. I know there were a bunch of 'm

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a look :)

s.osx.dependency "Cassette", '1.0.0-beta5'
s.dependency "Backtrace-PLCrashReporter", '1.5.3'

s.resources = 'Sources/**/*.xcdatamodeld'
Expand Down
412 changes: 211 additions & 201 deletions Backtrace.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ end

def shared_ios_mac_pods
shared_pods
pod 'Cassette', '1.0.0-beta5'
end

def shared_test_ios_mac_pods
Expand Down
13 changes: 4 additions & 9 deletions Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
PODS:
- Backtrace (1.7.4-beta2):
- Backtrace (1.7.5):
- Backtrace-PLCrashReporter (= 1.5.3)
- Cassette (= 1.0.0-beta5)
- Backtrace-PLCrashReporter (1.5.3)
- Cassette (1.0.0-beta5)
- Nimble (10.0.0)
- Quick (5.0.1)

DEPENDENCIES:
- Backtrace (from `./Backtrace.podspec`)
- Backtrace-PLCrashReporter (= 1.5.3)
- Cassette (= 1.0.0-beta5)
- Nimble (~> 10.0.0)
- Quick (~> 5.0.1)

SPEC REPOS:
trunk:
- Backtrace-PLCrashReporter
- Cassette
- Nimble
- Quick

Expand All @@ -26,12 +22,11 @@ EXTERNAL SOURCES:
:path: "./Backtrace.podspec"

SPEC CHECKSUMS:
Backtrace: c0124ca7e1a84bc7a3b3407671fb99a90be474e9
Backtrace: 1b471570061cb4740b42663aaf3e381177a317ce
Backtrace-PLCrashReporter: 71ddeba11834d2bcc3c19f357aaec7bf87131f89
Cassette: 074c6991391733888990dba728b7ffe00299a0a6
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
Quick: 749aa754fd1e7d984f2000fe051e18a3a9809179

PODFILE CHECKSUM: 2045466adc5eebf2fa4652c2a2c73ec6a81b89b3
PODFILE CHECKSUM: ec560ea7bd4dba9a68a30ffbb0b25db5e3491921

COCOAPODS: 1.11.3
COCOAPODS: 1.12.1
133 changes: 63 additions & 70 deletions Sources/Features/Breadcrumb/BacktraceBreadcrumbFileHelper.swift
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import Foundation
import Cassette

enum BacktraceBreadcrumbFileHelperError: Error {
case invalidFormat
}

@objc class BacktraceBreadcrumbFileHelper: NSObject {

/*
The underlying library CASQueueFile assigns a minimum of 4k (filled with zeroes).
Since we know that space will be allocated (and uploaded) anyways, set it as the minimum.
*/
private static let minimumQueueFileSizeBytes = 4096

private let maximumIndividualBreadcrumbSize: Int
private let maxQueueFileSizeBytes: Int
private let queue: CASQueueFile

private let queue: Queue<Any>
private let breadcrumbLogURL: URL

/** CASQueueFile is not thread safe, so all interactions with it should be done synchronously through this DispathQueue */
private let dispatchQueue = DispatchQueue(label: "io.backtrace.BacktraceBreadcrumbFileHelper@\(UUID().uuidString)")

public init(_ breadcrumbSettings: BacktraceBreadcrumbSettings) throws {
self.queue = try CASQueueFile.init(path: breadcrumbSettings.getBreadcrumbLogPath().path)


self.breadcrumbLogURL = try breadcrumbSettings.getBreadcrumbLogPath()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it can throw? Maybe it would be better to throw an error faster and here pass only data needed by BacktraceBreadcrumbFileHelper?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the file doesn't exist? we don't create it anywhere. We always write to a file. How about creating a start method that will do startup logic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the file will always exist;
getBreadcrumbLogPath() method will create a file if doesn't exist

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no good way to do this since BacktraceBreadcrumbSettings forces us to (try/unwrap), if for any reason the file is nil the files operations methods will catch the error and warn BacktraceLogger.

self.queue = Queue<Any>()
self.maximumIndividualBreadcrumbSize = breadcrumbSettings.maxIndividualBreadcrumbSizeBytes

if breadcrumbSettings.maxQueueFileSizeBytes < BacktraceBreadcrumbFileHelper.minimumQueueFileSizeBytes {
BacktraceLogger.warning("\(breadcrumbSettings.maxQueueFileSizeBytes) is smaller than the minimum of " +
"\(BacktraceBreadcrumbFileHelper.minimumQueueFileSizeBytes)" +
Expand All @@ -36,78 +32,75 @@ enum BacktraceBreadcrumbFileHelperError: Error {

super.init()
}

func addBreadcrumb(_ breadcrumb: [String: Any]) -> Bool {
let text: String
do {
text = try convertBreadcrumbIntoString(breadcrumb)
} catch {
BacktraceLogger.warning("\(error.localizedDescription) \nWhen converting breadcrumb to string")
return false
}

let textBytes = Data(text.utf8)
if textBytes.count > maximumIndividualBreadcrumbSize {
BacktraceLogger.warning(
"Discarding breadcrumb that was larger than the maximum specified (\(maximumIndividualBreadcrumbSize).")
return false
}

do {
try dispatchQueue.sync {
// Keep removing until there's enough space to add the new breadcrumb (leaving 512 bytes room)
while (queueByteSize() + textBytes.count) > (maxQueueFileSizeBytes - 512) {
try queue.pop(1, error: ())
// Serialize breadcrumb: [String: Any] into Data
let breadcrumbJsonData = try JSONSerialization.data(withJSONObject: breadcrumb)
// Serialize Data into a JSON string
guard let breadcrumbJsonString = String(data: breadcrumbJsonData, encoding: .utf8) else {
BacktraceLogger.warning("Error when adding breadcrumb to file")
return false
}
// Calculate the size of the breadcrumb and add it to queue
let breadcrumbSize = breadcrumbJsonData.count
// Check if breadcrumb size is larger than the maximum specified
if breadcrumbSize > maximumIndividualBreadcrumbSize {
BacktraceLogger.warning(
"Discarding breadcrumb that was larger than the maximum specified (\(maximumIndividualBreadcrumbSize).")
return false
}
// Store breadcrumb Json String and size in Dictionary [String : Any]
let queueBreadcrumb = ["breadcrumbJson": breadcrumbJsonString, "size": breadcrumbSize] as [String : Any]
// Queue breacrumb
queue.enqueue(queueBreadcrumb)
// Iterate over the queue from newest to oldest breadcrumb and build an array of encoded strings
var breadcrumbsArray = [String]()
var size = 0
for index in (0..<queue.count).reversed() {
guard let queueBreadcrumb = queue.allElements()[index] as? [String: Any] else {
BacktraceLogger.warning("Error weh fetching breacrumbs from queue")
return false
}
guard let breadcrumbSize = queueBreadcrumb["size"] as? Int else {
BacktraceLogger.warning("Error when adding breadcrumbSize to array")
return false
}
// Pop last element if size id greater than maxQueueFileSizeBytes
if size + breadcrumbSize > maxQueueFileSizeBytes && !queue.isEmpty {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should break here - why? there will be a chance we omit one breadcrumb but add a few older one and this algorithm will be fine with it. It would be better if we will have breadcrumbs in the right order and we won't miss any

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok , adding a break here.

queue.pop()
} else {
guard let breadcrumbJsonData = queueBreadcrumb["breadcrumbJson"] as? String else {
BacktraceLogger.warning("Error when adding breadcrumbJson to array")
return false
}
breadcrumbsArray.append(breadcrumbJsonData)
size += breadcrumbSize
}

try queue.add(textBytes, error: ())
}
// Write breadcrumbs to file
let breadcrumbString = "[\(breadcrumbsArray.joined(separator: ","))]"
try breadcrumbString.write(to: self.breadcrumbLogURL, atomically: true, encoding: .utf8)
return true
} catch {
BacktraceLogger.warning("\(error.localizedDescription) \nWhen adding breadcrumb to file")
BacktraceLogger.warning("Error when adding breadcrumb to file: \(error)")
return false
}

return true
}

func clear() -> Bool {
do {
try dispatchQueue.sync {
try queue.clearAndReturnError()
}
} catch {
BacktraceLogger.warning("\(error.localizedDescription) \nWhen clearing breadcrumb file")
return false
dispatchQueue.sync {
queue.clear()
clearBreadcrumbLogFile(at:self.breadcrumbLogURL)
}
return true
}
}

extension BacktraceBreadcrumbFileHelper {

func convertBreadcrumbIntoString(_ breadcrumb: Any) throws -> String {
let breadcrumbData = try JSONSerialization.data( withJSONObject: breadcrumb, options: [])
if let breadcrumbText = String(data: breadcrumbData, encoding: .utf8) {
return "\n\(breadcrumbText)\n"
}
throw BacktraceBreadcrumbFileHelperError.invalidFormat
}

func queueByteSize() -> Int {
// This is the current fileLength of the QueueFile
guard let fileLength = queue.value(forKey: "fileLength") as? Int else {
BacktraceLogger.error("fileLength is not an Int, this is unexpected!")
return maxQueueFileSizeBytes
}

// let usedBytes = queue.value(forKey: "usedBytes") as? Int

// This is the remaining bytes before the file needs to be expanded
guard let remainingBytes = queue.value(forKey: "remainingBytes") as? Int else {
BacktraceLogger.error("remainingBytes is not an Int, this is unexpected!")
return 0

func clearBreadcrumbLogFile(at breadcrumbLogURL: URL) {
do {
try "".write(to: breadcrumbLogURL, atomically: false, encoding: .utf8)
} catch {
BacktraceLogger.warning("Error clearing breadcrumb log file at: \(breadcrumbLogURL) - \(error.localizedDescription)")
}

return fileLength - remainingBytes
}
}
45 changes: 45 additions & 0 deletions Sources/Features/Breadcrumb/QueueFile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it QueueFile? I believe its a generic queue instead. Can we rename the file to Queue or something like that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queue it is!


@objcMembers
public class Queue<T>: NSObject {
private var elements: [T] = []

func enqueue(_ element: T) {
elements.append(element)
}

func dequeue() -> T? {
if elements.isEmpty {
return nil
} else {
return elements.removeFirst()
}
}

func peek() -> T? {
return elements.first
}

func pop() -> T? {
guard !elements.isEmpty else {
return nil
}
return elements.popLast()
}

public func allElements() -> [T] {
return elements
}

func clear() {
elements.removeAll()
}

var isEmpty: Bool {
return elements.isEmpty
}

var count: Int {
return elements.count
}
}
15 changes: 0 additions & 15 deletions Tests/BacktraceBreadcrumbTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,6 @@ final class BacktraceBreadcrumbTests: QuickSpec {
var writeIndex = 0
while writeIndex < 1000 {
let text = "this is Breadcrumb number \(writeIndex)"
// submit a task to the queue for background execution
DispatchQueue.global().async(group: group, execute: {
expect { breadcrumbs.addBreadcrumb(text) }.to(beTrue())
Copy link
Contributor

@vlussenburg vlussenburg May 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this background task, this test doesn't do what is intended to do (in fact, the entire loop is useless).

We had lots of problems with multithreaded behavior corrupting the file, so beware!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll seep a close eye on multithreaded behaviour, thank you!

})
writeIndex += 1
}

Expand All @@ -192,21 +188,10 @@ final class BacktraceBreadcrumbTests: QuickSpec {
}
}

// Why the - 1?
// Because one line is liable to get mangled by the wrapping - half will
// be at the bottom and half at the top of the circular file.
// Currently, we accept we lose this Breadcrumb in the UI - it will still be in the file
// for manual inspection.
let expectedNumberOfMatches = writeIndex - wrapIndex - 1
expect(matches).to(beGreaterThanOrEqualTo(expectedNumberOfMatches),
Copy link
Contributor

@vlussenburg vlussenburg May 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is important. I would consider adjusting it, not deleting it. The -1 may no longer be necessary due to the Cassette dependency being removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is specifically there to make sure the file rolls over, so the number of expected matches are really important. Otherwise the entire test doesn't do anything the other (smaller) tests already do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this one for the same reason, I'll adjust it.
Thank you :)

description: "Not enough (\(matches) out of \(expectedNumberOfMatches)) " +
"breadcrumb matches found in breadcrumbs file: \n\(breadcrumbText)")

let attr = try FileManager.default.attributesOfItem(atPath: self.breadcrumbLogPath(false))
let fileSize = attr[FileAttributeKey.size] as? Int
let requestedSize = settings.maxQueueFileSizeBytes
expect { fileSize }.to(beLessThanOrEqualTo(requestedSize))
expect { fileSize }.to(beGreaterThanOrEqualTo(requestedSize - 1000))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is important. I would consider adjusting it, not deleting it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

}
}
}
Expand Down