Performance

This commit is contained in:
Rukira 2026-03-04 15:00:31 +00:00
parent 923835203a
commit 44656c81d5
3 changed files with 45 additions and 10 deletions

View file

@ -17,12 +17,29 @@ class BackupEntry(
val sizeBytes: Long by lazy { sizeComputer() } val sizeBytes: Long by lazy { sizeComputer() }
} }
data class BackupMetrics(val fileCount: Int, val sizeBytes: Long)
object BackupHistory { object BackupHistory {
// Used internally for parsing — java.time formatter since kotlinx.datetime // Used internally for parsing — java.time formatter since kotlinx.datetime
// doesn't have custom format patterns built in // doesn't have custom format patterns built in
private val JAVA_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss") private val JAVA_TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
/**
* Counts backups without parsing timestamps or sorting just matches the naming pattern.
*/
fun countBackups(backupDir: File): Int {
if (!backupDir.exists() || !backupDir.isDirectory) return 0
return backupDir.listFiles()?.count { file ->
val name = file.nameWithoutExtension
val isZip = file.extension.equals("zip", ignoreCase = true) && file.isFile
val isDir = file.isDirectory
if (!isZip && !isDir) return@count false
try { java.time.LocalDateTime.parse(name, JAVA_TIMESTAMP_FORMAT); true }
catch (_: DateTimeParseException) { false }
} ?: 0
}
/** /**
* Lists all existing backups in the backup directory, sorted newest first. * Lists all existing backups in the backup directory, sorted newest first.
*/ */
@ -37,13 +54,18 @@ object BackupHistory {
} }
/** /**
* Returns the file count for a backup entry without computing its full size. * Computes file count and size in a single pass over the backup tree.
*/ */
fun fileCount(entry: BackupEntry): Int { fun computeMetrics(entry: BackupEntry): BackupMetrics {
return if (entry.isCompressed) { return if (entry.isCompressed) {
java.util.zip.ZipFile(entry.path).use { it.size() } java.util.zip.ZipFile(entry.path).use { zip ->
BackupMetrics(zip.size(), entry.path.length())
}
} else { } else {
entry.path.walkTopDown().count { it.isFile } var count = 0
var size = 0L
entry.path.walkTopDown().filter { it.isFile }.forEach { count++; size += it.length() }
BackupMetrics(count, size)
} }
} }

View file

@ -46,6 +46,19 @@ object BackupScheduler {
if (schedulerJob?.isActive == true) return if (schedulerJob?.isActive == true) return
logger.info { "Backup scheduler started" } logger.info { "Backup scheduler started" }
// Eagerly set timestamps so StatusScreen has data immediately.
// Only set lastBackupTime and nextBackupTime here (cheap).
// The full lastBackupResult (fileCount, sizeBytes) is filled by the async path
// since fileCount() walks the directory tree and would block the main thread.
val config = ConfigManager.config.value
if (config.isConfigured && config.backupPath != null) {
val backups = BackupHistory.listBackups(File(config.backupPath))
if (backups.isNotEmpty()) {
_state.update { it.copy(lastBackupTime = backups.first().timestamp) }
}
updateNextBackupTime(TimeZone.currentSystemDefault())
}
schedulerJob = scope.launch { schedulerJob = scope.launch {
// Collect progress from BackupEngine // Collect progress from BackupEngine
launch { launch {
@ -65,23 +78,23 @@ object BackupScheduler {
} }
// On first config emission, restore last backup info from disk // On first config emission, restore last backup info from disk
if (_state.value.lastBackupTime == null && config.backupPath != null) { if (_state.value.lastBackupResult == null && config.backupPath != null) {
val backups = BackupHistory.listBackups(File(config.backupPath)) val backups = BackupHistory.listBackups(File(config.backupPath))
if (backups.isNotEmpty()) { if (backups.isNotEmpty()) {
val latest = backups.first() val latest = backups.first()
val fileCount = BackupHistory.fileCount(latest) val metrics = BackupHistory.computeMetrics(latest)
_state.update { _state.update {
it.copy( it.copy(
lastBackupTime = latest.timestamp, lastBackupTime = latest.timestamp,
lastBackupResult = BackupResult.Success( lastBackupResult = BackupResult.Success(
backupPath = latest.path, backupPath = latest.path,
fileCount = fileCount, fileCount = metrics.fileCount,
sizeBytes = latest.sizeBytes, sizeBytes = metrics.sizeBytes,
durationMs = 0, durationMs = 0,
), ),
) )
} }
logger.info { "Restored last backup info from disk: ${latest.path.name} ($fileCount files)" } logger.info { "Restored last backup info from disk: ${latest.path.name} (${metrics.fileCount} files)" }
} }
} }

View file

@ -43,7 +43,7 @@ class StatusViewModel : ViewModel() {
BackupScheduler.state.map { it.lastBackupResult }.distinctUntilChanged(), BackupScheduler.state.map { it.lastBackupResult }.distinctUntilChanged(),
) { backupPath, _ -> ) { backupPath, _ ->
if (backupPath != null) { if (backupPath != null) {
BackupHistory.listBackups(File(backupPath)).size BackupHistory.countBackups(File(backupPath))
} else { } else {
0 0
} }