Skip to content

Commit

Permalink
Merge pull request #1571 from planetary-social/background-database-cl…
Browse files Browse the repository at this point in the history
…eanup

Run database cleanup in the background
  • Loading branch information
mplorentz authored Oct 9, 2024
2 parents 42146a5 + 1424d00 commit df4a40f
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Decreased the opacity on disabled buttons.

### Internal Changes
- Moved the database cleanup routine into a background execution task. [#1426](https://github.com/planetary-social/nos/issues/1426)
- Fix the Crowdin GitHub integration by using the official GitHub action. [#1520](https://github.com/planetary-social/nos/issues/1520)
- Update Xcode to version 15.4, adding compatibility for Xcode 16.
- Reduced spammy "Failed to parse Follow" log messages.
Expand Down
8 changes: 7 additions & 1 deletion Nos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2863,8 +2863,9 @@
GCC_PREPROCESSOR_DEFINITIONS = "STAGING=1";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nos/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Nos Staging";
INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand Down Expand Up @@ -3038,6 +3039,7 @@
INFOPLIST_FILE = Nos/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand Down Expand Up @@ -3307,7 +3309,9 @@
GCC_OPTIMIZATION_LEVEL = 0;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nos/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand Down Expand Up @@ -3362,7 +3366,9 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Nos/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Nos Dev";
INFOPLIST_KEY_NSCameraUsageDescription = "Nos can access camera to allow users to post photos directly.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Nos uses the microphone when you are record a video from within the app.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand Down
41 changes: 41 additions & 0 deletions Nos/Controller/PersistenceController.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CoreData
import Logger
import Dependencies
import BackgroundTasks

final class PersistenceController {

Expand Down Expand Up @@ -121,6 +122,14 @@ final class PersistenceController {
private static func saveVersionToDisk(_ newVersion: Int) {
UserDefaults.standard.set(newVersion, forKey: Self.versionKey)
}
}

// MARK: - Maintenance & Cleanup

extension PersistenceController {

/// The ID of the background task that runs the `DatabaseCleaner`. Needs to match the value in Info.plist.
static let cleanupBackgroundTaskID = "com.verse.nos.database-cleaner"

/// Cleans up unneeded entities from the database. Our local database is really just a cache, and we need to
/// invalidate old items to keep it from growing indefinitely.
Expand All @@ -140,6 +149,36 @@ final class PersistenceController {
}
}

/// Tells iOS to wake our app up in the background to run `cleanupEntities()` periodically. This tends to run
/// once a day when the device is connected to power.
func scheduleBackgroundCleanupTask() {
@Dependency(\.analytics) var analytics
@Dependency(\.crashReporting) var crashReporting
let scheduler = BGTaskScheduler.shared

scheduler.register(forTaskWithIdentifier: Self.cleanupBackgroundTaskID, using: nil) { task in

task.expirationHandler = {
analytics.databaseCleanupTaskExpired()
}

Task {
await self.cleanupEntities()
task.setTaskCompleted(success: true)
}
}

let request = BGProcessingTaskRequest(identifier: Self.cleanupBackgroundTaskID)
request.requiresNetworkConnectivity = false
request.requiresExternalPower = false

do {
try scheduler.submit(request)
} catch {
crashReporting.report("Unable to schedule background task: \(error)")
}
}

static func databaseStatistics(from context: NSManagedObjectContext) async throws -> [(String, Int)] {
try await context.perform {
var statistics = [String: Int]()
Expand All @@ -160,6 +199,8 @@ final class PersistenceController {
}
}

// MARK: - Testing & Debug

#if DEBUG
extension PersistenceController {
func loadSampleData(context: NSManagedObjectContext) async throws {
Expand Down
5 changes: 5 additions & 0 deletions Nos/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>processing</string>
</array>
<key>UNS_API_KEY</key>
<string>$(UNS_API_KEY)</string>
Expand All @@ -63,5 +64,9 @@
<string>Nos uses the microphone when you are record a video from within the app.</string>
<key>WALLET_CONNECT_PROJECT_ID</key>
<string>$(WALLET_CONNECT_PROJECT_ID)</string>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.verse.nos.database-cleaner</string>
</array>
</dict>
</plist>
4 changes: 1 addition & 3 deletions Nos/NosApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct NosApp: App {
// hack to fix confirmationDialog color issue
// https://github.com/planetary-social/nos/issues/1064
UIView.appearance(whenContainedInInstancesOf: [UIAlertController.self]).tintColor = .systemBlue
persistenceController.scheduleBackgroundCleanupTask()
}

var body: some Scene {
Expand All @@ -33,9 +34,6 @@ struct NosApp: App {
.environment(currentUser)
.environmentObject(pushNotificationService)
.onOpenURL { DeepLinkService.handle($0, router: router) }
.task {
await persistenceController.cleanupEntities()
}
.onChange(of: scenePhase) { _, newPhase in
switch newPhase {
case .inactive:
Expand Down
12 changes: 12 additions & 0 deletions Nos/Service/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ class Analytics {
track("Database Statistics", properties: properties)
}

func databaseCleanupStarted() {
track("Database Cleanup Started")
}

func databaseCleanupTaskExpired() {
track("Database Cleanup Task Expired")
}

func databaseCleanupCompleted(duration: TimeInterval) {
track("Database Cleanup Completed", properties: ["duration": duration])
}

func logout() {
track("Logged out")
postHog?.reset()
Expand Down
2 changes: 2 additions & 0 deletions Nos/Service/DatabaseCleaner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum DatabaseCleaner {
@Dependency(\.analytics) var analytics

let startTime = Date.now
analytics.databaseCleanupStarted()
Log.info("Starting Core Data cleanup...")

Log.info("Database statistics: \(try await PersistenceController.databaseStatistics(from: context))")
Expand Down Expand Up @@ -78,6 +79,7 @@ enum DatabaseCleaner {

let elapsedTime = Date.now.timeIntervalSince1970 - startTime.timeIntervalSince1970
Log.info("Finished Core Data cleanup in \(elapsedTime) seconds.")
analytics.databaseCleanupCompleted(duration: elapsedTime)
}

/// This converts old hydrated events back to stubs. We do this because EventReferences can form long chains
Expand Down

0 comments on commit df4a40f

Please sign in to comment.