Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Backups/Archiving/BackupArchiveProgress.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

/// Tracks progress of a Backup export, as a fraction of the number of database
/// rows we've exported so far over the approximate number we expect to.
/// - Note
/// Number of exported database rows is not a perfect metric for time spent, as
/// some rows require more time and work than others.
public struct BackupArchiveExportProgress {
    private let progressSource: OWSProgressSource

    public static func prepare(
        sink: OWSProgressSink,
        db: any DB,
    ) async throws -> Self {
        var estimatedFrameCount = try db.read { tx in
            // Get all the major things we iterate over. It doesn't have
            // to be perfect; we'll skip some of these and besides they're
            // all weighted evenly. Its just an estimate.
            return try
                SignalRecipient.fetchCount(tx.database)
                + TSThread.fetchCount(tx.database)
                + InteractionRecord.fetchCount(tx.database)
                + CallLinkRecord.fetchCount(tx.database)
                + StickerPackRecord.fetchCount(tx.database)
        }
        // Add a fixed extra amount for:
        // * header frame
        // * self recipient
        // * account data frame
        // * release notes channel
        estimatedFrameCount += 4

        let progressSource = await sink.addSource(
            withLabel: "Backup Export",
            unitCount: UInt64(estimatedFrameCount),
        )
        return BackupArchiveExportProgress(progressSource: progressSource)
    }

    public func didExportFrame() {
        progressSource.incrementCompletedUnitCount(by: 1)
    }

    public func didCloseStream() {
        // We used an estimate to populate the unit count originally, so make
        // sure we complete the progress when we're done.
        progressSource.complete()
    }
}

// MARK: -

/// Tracks the progress of importing frames from a Backup, as a fraction of the
/// total number of bytes read from the Backup file so far.
/// - Note
/// Number of bytes read is not a perfect metric for time spent, as some frames
/// require more time and work than others.
public struct BackupArchiveImportFramesProgress {
    private let progressSource: OWSProgressSource

    public static func prepare(
        sink: OWSProgressSink,
        fileUrl: URL,
    ) async throws -> Self {
        let totalByteCount = try OWSFileSystem.fileSize(of: fileUrl)

        let progressSource = await sink.addSource(
            withLabel: "Backup Import: Frame Restore",
            unitCount: totalByteCount,
        )
        return BackupArchiveImportFramesProgress(
            progressSource: progressSource,
        )
    }

    public func didReadBytes(count byteLength: Int) {
        guard let byteLength = UInt64(exactly: byteLength) else {
            owsFailDebug("How did we get such a huge byte length?")
            return
        }
        if byteLength > 0 {
            progressSource.incrementCompletedUnitCount(by: byteLength)
        }
    }
}

// MARK: -

public class BackupArchiveImportRecreateIndexesProgress {
    private enum Constants {
        static let progressSourceUnitCount: UInt64 = .max
    }

    @Atomic
    private var progressSource: OWSProgressSource
    private var updateProcessPeriodicallyTask: Task<Void, Error>?

    private init(progressSource: OWSProgressSource) {
        self.progressSource = progressSource
    }

    public static func prepare(
        sink: OWSProgressSink,
    ) async -> BackupArchiveImportRecreateIndexesProgress {
        let progressSource = await sink.addSource(
            withLabel: "Backup Import: Recreate Indexes",
            unitCount: Constants.progressSourceUnitCount,
        )
        return BackupArchiveImportRecreateIndexesProgress(
            progressSource: progressSource,
        )
    }

    /// Start approximating progress towards recreating indexes during a Backup
    /// import.
    ///
    /// SQLite doesn't report progress as it creates a database index, so we
    /// can't report progress precisely. Instead, we'll increment progress
    /// automatically over time, with a rough heuristic for how much progress we
    /// made during that time based on the number of restored frames.
    ///
    /// - Important
    /// Callers must pair a call to this method with a call to
    /// ``didFinishIndexRecreation()``.
    public func willStartIndexRecreation(totalFramesRestored: UInt64) {
        owsPrecondition(updateProcessPeriodicallyTask == nil)

        // Ballpark that we can recreate indexes for 5k frames-worth of database
        // rows per second. This number will depend on external factors like
        // device CPU as well as internal factors like how many indexes we're
        // creating.
        let framesEstimatePerSecond = 5_000
        let _unitCountPerSecond = Double(framesEstimatePerSecond) / Double(totalFramesRestored) * Double(Constants.progressSourceUnitCount)
        let unitCountPerSecond = UInt64(clamping: _unitCountPerSecond)

        updateProcessPeriodicallyTask = Task {
            while true {
                try Task.checkCancellation()
                try await Task.sleep(nanoseconds: NSEC_PER_SEC)

                progressSource.incrementCompletedUnitCount(by: unitCountPerSecond)
            }
        }
    }

    /// Finish approximating progress towards recreating indexes during a Backup
    /// import.
    public func didFinishIndexRecreation() {
        updateProcessPeriodicallyTask?.cancel()
        updateProcessPeriodicallyTask = nil

        progressSource.complete()
    }
}