Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/src/main/java/app/gamenative/data/SteamAppPicsMeta.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.gamenative.data

import androidx.room.ColumnInfo

// Slimmed-down projection of the columns the PICS collectors actually read when
// deciding whether an app needs (re)processing. Avoids pulling the full SteamApp
// row — including the large depots/config/UFS JSON blobs — into memory just to
// compare change numbers for tens of thousands of apps.
data class SteamAppPicsMeta(
val id: Int,
@ColumnInfo("package_id")
val packageId: Int,
@ColumnInfo("last_change_number")
val lastChangeNumber: Int,
@ColumnInfo("ufs_parse_version")
val ufsParseVersion: Int,
)
10 changes: 10 additions & 0 deletions app/src/main/java/app/gamenative/data/SteamLicenseForPics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.gamenative.data

import androidx.room.ColumnInfo

// Slimmed down type for retrieving licences for processing.
data class SteamLicenseForPics(
val packageId: Int,
@ColumnInfo("access_token")
val accessToken: Long,
)
10 changes: 10 additions & 0 deletions app/src/main/java/app/gamenative/data/SteamLicenseStub.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.gamenative.data

import androidx.room.ColumnInfo

// Smallest possible stub for steam license comparisons
data class SteamLicenseStub(
val packageId: Int,
@ColumnInfo("last_change_number") val lastChangeNumber: Int,
@ColumnInfo("access_token") val accessToken: Long,
)
55 changes: 54 additions & 1 deletion app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import app.gamenative.data.SteamApp
import app.gamenative.data.SteamAppPicsMeta
import app.gamenative.service.SteamService.Companion.INVALID_PKG_ID
import kotlin.math.min
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
Expand Down Expand Up @@ -199,5 +201,56 @@ interface SteamAppDao {
suspend fun findSteamAppWithInstallDir(dirName: String): List<SteamApp>

@Query("SELECT * FROM steam_app WHERE id IN (:appIds)")
suspend fun findSteamAppWithAppIds(appIds: List<Int>): List<SteamApp>
suspend fun _findSteamAppWithAppIds(appIds: List<Int>): List<SteamApp>

// batched to stay under SQLite's 999 bind-variable limit
@Transaction
suspend fun findSteamAppWithAppIds(appIds: List<Int>): List<SteamApp> {
if (appIds.isEmpty()) return emptyList()
val results = mutableListOf<SteamApp>()
for (chunkStart in appIds.indices step SQLITE_MAX_VARS) {
val chunkEnd = min(chunkStart + SQLITE_MAX_VARS, appIds.size)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
results += _findSteamAppWithAppIds(appIds.subList(chunkStart, chunkEnd))
}
return results
}

@Query(
"SELECT id, package_id, last_change_number, ufs_parse_version " +
"FROM steam_app WHERE id IN (:appIds)",
)
suspend fun _findAppPicsMeta(appIds: List<Int>): List<SteamAppPicsMeta>

@Query("UPDATE steam_app SET package_id = :packageId WHERE id IN (:appIds)")
suspend fun _updatePackageIdForApps(packageId: Int, appIds: List<Int>)

/**
* Reassigns package ownership for a set of apps in one statement per chunk,
* instead of read-modify-writing each full SteamApp row. Batched to stay
* under SQLite's 999-parameter ceiling.
*/
@Transaction
suspend fun updatePackageIdForApps(packageId: Int, appIds: List<Int>) {
if (appIds.isEmpty()) return
for (chunkStart in appIds.indices step SQLITE_MAX_VARS) {
val chunkEnd = min(chunkStart + SQLITE_MAX_VARS, appIds.size)
_updatePackageIdForApps(packageId, appIds.subList(chunkStart, chunkEnd))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Lightweight projection of the columns the PICS collectors read to decide
* whether an app needs (re)processing. Batched to stay under SQLite's
* 999-parameter ceiling; avoids deserializing the full row/blobs per app.
*/
@Transaction
suspend fun findAppPicsMeta(appIds: List<Int>): List<SteamAppPicsMeta> {
if (appIds.isEmpty()) return emptyList()
val results = mutableListOf<SteamAppPicsMeta>()
for (chunkStart in appIds.indices step SQLITE_MAX_VARS) {
val chunkEnd = min(chunkStart + SQLITE_MAX_VARS, appIds.size)
results += _findAppPicsMeta(appIds.subList(chunkStart, chunkEnd))
}
return results
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/app/gamenative/db/dao/SteamLicenseDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import app.gamenative.data.SteamLicense
import app.gamenative.data.SteamLicenseForPics
import app.gamenative.data.SteamLicenseStub
import kotlin.math.min

val SQLITE_MAX_VARS = 999
Expand All @@ -29,6 +31,13 @@ interface SteamLicenseDao {
@Query("SELECT * FROM steam_license")
suspend fun getAllLicenses(): List<SteamLicense>

//lightweight difffs for steamlicences
@Query("SELECT packageId, last_change_number, access_token FROM steam_license")
suspend fun getLicenseStubs(): List<SteamLicenseStub>

@Query("SELECT packageId, access_token FROM steam_license")
suspend fun getLicensesForPics(): List<SteamLicenseForPics>

@Query("SELECT * FROM steam_license WHERE packageId = :packageId")
suspend fun findLicense(packageId: Int): SteamLicense?

Expand Down
31 changes: 31 additions & 0 deletions app/src/main/java/app/gamenative/di/DatabaseModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package app.gamenative.di

import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import app.gamenative.db.DATABASE_NAME
import app.gamenative.db.PluviaDatabase
import app.gamenative.db.dao.AppInfoDao
Expand All @@ -19,6 +21,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.util.concurrent.Executors
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
Expand All @@ -36,6 +39,34 @@ class DatabaseModule {
ROOM_MIGRATION_V23_to_V24,
)
.fallbackToDestructiveMigration(true)
// Use unbounded thread pools for reads/writes so a long-running query (e.g. the
// library COUNT/paged load during a large PICS sync) can't starve the default
// single-threaded executors and deadlock the connection pool. SQLite still
// serializes writers, so this only removes the scheduling deadlock.
.setQueryExecutor(Executors.newCachedThreadPool())
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
.setTransactionExecutor(Executors.newCachedThreadPool())
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
.addCallback(object : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
// These indexes are not declared in @Entity (which would require a schema
// migration) — they're created here so we can add them without a version bump
// while keeping upstream compatibility. IF NOT EXISTS makes them idempotent.
//
// dlc_for_app_id: the OWNED_APPS_WHERE DLC EXISTS arm joins back into
// steam_app on this column. Without an index, SQLite scans all rows per
// outer row that fails the direct-license check, turning the COUNT(*) query
// O(N²) and causing 100+ second runtimes that exhaust the connection pool.
db.execSQL(
"CREATE INDEX IF NOT EXISTS idx_steam_app_dlc_for_app_id " +
"ON steam_app(dlc_for_app_id)",
)
// package_id: used in the WHERE clause to exclude INVALID_PKG_ID rows and
// as the lookup key for the direct-license EXISTS arm.
db.execSQL(
Comment thread
phobos665 marked this conversation as resolved.
"CREATE INDEX IF NOT EXISTS idx_steam_app_package_id " +
"ON steam_app(package_id)",
)
}
})
.build()
}

Expand Down
Loading
Loading