-
Notifications
You must be signed in to change notification settings - Fork 4
LOOP-1201 Export of Critical Event Logs #242
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
LOOP-1201 Export of Critical Event Logs #242
Conversation
- https://tidepool.atlassian.net/browse/LOOP-1201 - Add critical event log export to several Core Data stores - Add Encodable to various Core Data models - Add modificationCounter to DeviceLogEntry - Add JSONStreamEncoder - Replace StoredSettings.InsulinModel with StoredInsulinModel - Fix PumpManagerStatus Codable for optional pumpStatusHighlight and pumpLifecycleProgress
|
Related LoopWorkspace PR for demonstration of build: https://github.com/tidepool-org/LoopWorkspace/pull/250 |
ps2
left a comment
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.
LGTM. If these operations are really background operations, and we no longer need progress estimation; I'd recommend that be cleaned up; there is a lot of code around progress, and possibly some significant io due to the count(*) operations that I think are only necessary for progress reporting.
|
|
||
| // MARK: - Critical Event Log Export | ||
|
|
||
| extension CarbStore: CriticalEventLog { |
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.
Agree with Rick; if this work is really isolated to an extension, putting it into a separate file might make this a little more navigable.
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.
Okay, I can do this, but if we are going this way, we should make this a general, well-known policy. We haven't been doing this before.
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.
I don't feel that strongly, though, FWIW
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.
Yeah, it's not a big issue for me either; just seems a little more navigable.
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.
I'd be fine if we don't do this here.
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.
The code in this extension uses the managed object context, which is a private property. To move this extension out of the main file would require changing the property access from private to internal. I believe keeping the property private is more important than moving the extension to a separate file. Going to leave as-is unless anyone objects.
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.
✅
LoopKit/CriticalEventLog.swift
Outdated
|
|
||
| import Foundation | ||
|
|
||
| public protocol EstimatedDurationProgressor { |
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.
The idea of cancelable progress and rolled up tasks seems really similar to the built in Progress class: https://developer.apple.com/documentation/foundation/progress
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.
On a larger scale; if this is happening in the background, do we need progress reporting?
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.
We need progress for the full export in case the nightly exports have (for whatever reason) not completed. It is an edge case, but it definitely needs progress. (Also, with my current work looking into a different zip implementation, it looks like it may take longer than 3-5 seconds for the full export to complete so we at least need a progress bar.)
I did look into Progress and at the time did not think it appropriate. I will revisit.
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.
Updated to use Progress. Required a number of downstream changes, but should be good now. ✅
| let request: NSFetchRequest<DeviceLogEntry> = DeviceLogEntry.fetchRequest() | ||
| request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate) | ||
|
|
||
| let objectCount = try self.managedObjectContext.count(for: request) |
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.
count(*) operations often require a full table scan (and thus a lot of io). I'm not sure that's the case here or how heavily you're relying on this being performant, but thought I'd mention it. Count can be faster than an actual fetch because you save on hydration; but since IO is usually the slowest part of the query, the count operation might be nearly as slow an actual fetch of all the objects involved.
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.
We need progress (as discussed above) and progress highly depends upon the amount of data, thus the need for the count. The count has only a little effect on overall duration. (It was necessary to add indexes on dosing decisions and device comms table for exporting in general, not specific to progress, so this solves most of the issue).
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.
✅
rickpasetto
left a comment
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.
Wow, this is a lot of work! Great job, Darin!
Sorry, but I have a few questions that I need answered before I can approve. Happy to hop on a call to discuss.
|
|
||
| // MARK: - Critical Event Log Export | ||
|
|
||
| extension CarbStore: CriticalEventLog { |
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.
I don't feel that strongly, though, FWIW
LoopKit/CriticalEventLog.swift
Outdated
| /// - stream: The output stream to write the critical event log to. Typically writes JSON UTF-8 text. | ||
| /// - progressor: The estimated duration progress to use to check if cancelled and report progress. | ||
| /// - Returns: Any error that occurs during the export, or nil if successful. | ||
| func export(startDate: Date, endDate: Date, to stream: OutputStream, progressor: EstimatedDurationProgressor) -> Error? |
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.
[nit] Consider returning Result<Void, Error> or, if more appropriate, having the function throw
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.
Per discussion, going to leave as-is.
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.
✅
|
|
||
| import Foundation | ||
|
|
||
| extension OutputStream { |
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.
[nit] unit test for this?
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.
Good catch, done. ✅
LoopKit/InsulinKit/DoseStore.swift
Outdated
| return result! | ||
| } | ||
|
|
||
| public func export(startDate: Date, endDate: Date, to stream: OutputStream, progressor: EstimatedDurationProgressor) -> Error? { |
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.
These export(...) functions on the Stores are so similar. Is there a way to factor them into common code?
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.
I gave it a try and found there are too many differences between the different stores for it to be straightforward. It is possible, of course, but there would have to be at least on "customizer" function to account for the store differences and the final code would be less understandable.
For example, each store counts each object as a different amount towards overall progress (which is because each object takes a different amount of time to query, serialize, and write - there is a very large difference in object size between, say, alerts and dosing decisions). So each store would have to customize this cost while the common code is iterating over and exporting each object.
Also, while most stores use a modificationCounter property to sort and query on, the CarbStore uses an anchorKey property. These properties are used to provide a stable sort for paging data during an export.
These differences and others would make the code much less understandable if we tried to make a common "export" function.
Long term we could work on making the stores more similar in functionality (and property names) and this might be easier.
✅
| self.syncIdentifier = syncIdentifier | ||
| } | ||
|
|
||
| public struct InsulinModel: Codable, Equatable { |
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.
Hooray!! 💯
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.
✅
|
|
||
| private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate { | ||
| var addedDatePredicate = NSPredicate(format: "addedDate >= %@", startDate as NSDate) | ||
| var supercededDatePredicate = NSPredicate(format: "supercededDate >= %@", startDate as NSDate) |
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.
I have a roughly similar question about this that I had with AlertStore. If you're looking for something "after" startDate, and supercededDate is never before addedDate, then why do you have to include it in the predicate?
Similarly for endDate: what if the record is modified with supercededDate that is later than an addedDate but endDate is in between them? i.e.: addedDate < endDate < supercededDate?
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.
Since historical exports are captured on a daily basis it is necessary to cover the use case of the carb object added during one daily period and the carb object superseded in a subsequent daily period. If endDate is nil, then the final predicate can simplified slightly at the expense of slightly more complicated code, but that is not the common use case. (Most exports are historical and those will have an endDate specified.) Going to keep as-is.
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.
✅
rickpasetto
left a comment
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.
LGTM! 🥍
Note: This will require @ps2 and one other reviewer. Also, I looked into breaking this into smaller PRs, but could not come up with a way to do so that made any sense. (I could have broken out the
Codableimplementations, but that doesn't make this PR any easier, just a bit smaller, and is not worth the work.)Note: All of the store implementations of
CriticalEventLogare very similar (with only a few differences unique to each store). Once you've understood one, you've understood 95% of the rest).