perf(teacher): lower sync memory footprint

- Reduce sync parallelism 5 -> 2 to cap peak heap while bulk-fetching
  all departments for a period
- Replace per-write rows (insert schedule/days/lessons separately) with a
  single @Transaction replaceSchedule() so Room invalidates observers once
  per department instead of ~4x each, cutting main-thread recompositions
  and intermediate allocations during the 38-department sync
This commit is contained in:
2026-09-20 02:43:54 +03:00
parent 1d55e84e96
commit 6189540acf
2 changed files with 27 additions and 24 deletions
@@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
@Dao
@@ -38,6 +39,16 @@ interface TeacherScheduleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertLessons(lessons: List<TeacherLessonEntity>)
@Transaction
suspend fun replaceSchedule(schedule: TeacherScheduleEntity, days: List<TeacherDayEntity>, lessons: List<TeacherLessonEntity>) {
val scheduleId = insertSchedule(schedule)
val daysWithScheduleId = days.map { it.copy(scheduleId = scheduleId) }
deleteDaysForSchedule(scheduleId)
val dayIds = insertDays(daysWithScheduleId)
val dayIdByDate = daysWithScheduleId.zip(dayIds).associate { (day, id) -> day.date to id }
insertLessons(lessons.map { lesson -> lesson.copy(dayId = dayIdByDate.getValue(lesson.date)) })
}
@Query(
"SELECT teacher_days.* FROM teacher_days INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
@@ -156,33 +156,15 @@ class TeacherScheduleRepositoryImpl(
val html = VyatsuHttpClient.getBytes(url).toString(Charsets.UTF_8)
val parsed = TeacherScheduleParser.parse(html)
val scheduleId = scheduleDao.insertSchedule(
TeacherScheduleEntity(
departmentId = target.departmentId,
season = season,
weekStart = target.weekStart,
weekEnd = target.weekEnd,
lastSyncedAt = now()
)
)
replaceDays(scheduleId, parsed.days)
}
private suspend fun replaceDays(scheduleId: Long, days: List<Day>) {
scheduleDao.deleteDaysForSchedule(scheduleId)
val dayEntities = days.map {
TeacherDayEntity(scheduleId = scheduleId, date = it.date.toEpochDay())
val dayEntities = parsed.days.map {
TeacherDayEntity(scheduleId = 0, date = it.date.toEpochDay())
}
val dayIds = scheduleDao.insertDays(dayEntities)
val dayIdByDate = dayEntities.zip(dayIds).associate { (day, id) ->
LocalDate.ofEpochDay(day.date) to id
}
val lessons = days.flatMap { day ->
val lessonEntities = parsed.days.flatMap { day ->
day.intervals.flatMapIndexed { slotIndex, slot ->
slot.lessons.mapIndexedNotNull { lessonIndex, lesson ->
val teacher = lesson.teacher ?: return@mapIndexedNotNull null
TeacherLessonEntity(
dayId = dayIdByDate.getValue(day.date),
dayId = 0,
date = day.date.toEpochDay(),
slotIndex = slotIndex,
start = slot.start.toMinutesOfDay(),
@@ -199,7 +181,17 @@ class TeacherScheduleRepositoryImpl(
}
}
}
scheduleDao.insertLessons(lessons)
scheduleDao.replaceSchedule(
TeacherScheduleEntity(
departmentId = target.departmentId,
season = season,
weekStart = target.weekStart,
weekEnd = target.weekEnd,
lastSyncedAt = now()
),
dayEntities,
lessonEntities
)
}
private suspend fun trimToRecent(keepDistinctPeriods: Int) {
@@ -262,7 +254,7 @@ class TeacherScheduleRepositoryImpl(
companion object {
private const val STALE_WINDOW_MS = 12 * 60 * 60 * 1000L
private const val PARALLELISM = 5
private const val PARALLELISM = 2
private const val TAG = "TeacherScheduleRepo"
private val RANGE_FMT = java.time.format.DateTimeFormatter.ofPattern("ddMMyyyy")
}