-
Notifications
You must be signed in to change notification settings - Fork 18
Native crumbs #111
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
Native crumbs #111
Changes from 2 commits
aaf7aa6
1d3d5b1
709c549
3d09e17
d66875a
d8ece9d
f592065
e851ab7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 { | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /* | ||
| 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() | ||
|
||
| self.queue = Queue<Any>() | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| self.maximumIndividualBreadcrumbSize = breadcrumbSettings.maxIndividualBreadcrumbSizeBytes | ||
|
|
||
| if breadcrumbSettings.maxQueueFileSizeBytes < BacktraceBreadcrumbFileHelper.minimumQueueFileSizeBytes { | ||
| BacktraceLogger.warning("\(breadcrumbSettings.maxQueueFileSizeBytes) is smaller than the minimum of " + | ||
| "\(BacktraceBreadcrumbFileHelper.minimumQueueFileSizeBytes)" + | ||
|
|
@@ -36,78 +32,75 @@ enum BacktraceBreadcrumbFileHelperError: Error { | |
|
|
||
| super.init() | ||
| } | ||
|
|
||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func addBreadcrumb(_ breadcrumb: [String: Any]) -> Bool { | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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") | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 { | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| BacktraceLogger.warning("Error weh fetching breacrumbs from queue") | ||
| return false | ||
| } | ||
| guard let breadcrumbSize = queueBreadcrumb["size"] as? Int else { | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| BacktraceLogger.warning("Error when adding breadcrumbSize to array") | ||
| return false | ||
| } | ||
| // Pop last element if size id greater than maxQueueFileSizeBytes | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if size + breadcrumbSize > maxQueueFileSizeBytes && !queue.isEmpty { | ||
|
||
| queue.pop() | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } 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) | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
|
|
||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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) | ||
melekr marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| 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 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import Foundation | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? { | ||
melekr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| guard !elements.isEmpty else { | ||
| return nil | ||
| } | ||
| return elements.popLast() | ||
| } | ||
|
|
||
| public func allElements() -> [T] { | ||
melekr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return elements | ||
| } | ||
|
|
||
| func clear() { | ||
| elements.removeAll() | ||
| } | ||
|
|
||
| var isEmpty: Bool { | ||
| return elements.isEmpty | ||
| } | ||
|
|
||
| var count: Int { | ||
| return elements.count | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
|
|
||
|
|
@@ -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), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed this one for the same reason, I'll adjust it. |
||
| 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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check is important. I would consider adjusting it, not deleting it.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will do |
||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 'mThere was a problem hiding this comment.
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 :)