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() }
}
data class BackupMetrics(val fileCount: Int, val sizeBytes: Long)
object BackupHistory {
// Used internally for parsing — java.time formatter since kotlinx.datetime
// doesn't have custom format patterns built in
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.
*/
@ -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) {
java.util.zip.ZipFile(entry.path).use { it.size() }
java.util.zip.ZipFile(entry.path).use { zip ->
BackupMetrics(zip.size(), entry.path.length())
}
} 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
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 {
// Collect progress from BackupEngine
launch {
@ -65,23 +78,23 @@ object BackupScheduler {
}
// 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))
if (backups.isNotEmpty()) {
val latest = backups.first()
val fileCount = BackupHistory.fileCount(latest)
val metrics = BackupHistory.computeMetrics(latest)
_state.update {
it.copy(
lastBackupTime = latest.timestamp,
lastBackupResult = BackupResult.Success(
backupPath = latest.path,
fileCount = fileCount,
sizeBytes = latest.sizeBytes,
fileCount = metrics.fileCount,
sizeBytes = metrics.sizeBytes,
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(),
) { backupPath, _ ->
if (backupPath != null) {
BackupHistory.listBackups(File(backupPath)).size
BackupHistory.countBackups(File(backupPath))
} else {
0
}