That fuck shit the fascists are using
at master 270 lines 8.0 kB view raw
1package org.tm.archive.database 2 3import android.annotation.SuppressLint 4import android.app.Application 5import android.content.ContentValues 6import net.zetetic.database.sqlcipher.SQLiteDatabase 7import net.zetetic.database.sqlcipher.SQLiteOpenHelper 8import org.signal.core.util.CursorUtil 9import org.signal.core.util.SqlUtil 10import org.signal.core.util.logging.Log 11import org.tm.archive.crypto.DatabaseSecret 12import org.tm.archive.crypto.DatabaseSecretProvider 13import org.tm.archive.database.model.LocalMetricsEvent 14import java.util.concurrent.TimeUnit 15 16/** 17 * Stores metrics for user events locally on disk. 18 * 19 * These metrics are only ever included in debug logs in an aggregate fashion (i.e. p50, p90, p99) and are never automatically uploaded anywhere. 20 * 21 * The performance of insertions is important, but given insertion frequency isn't crazy-high, we can also optimize for retrieval performance. 22 * SQLite isn't amazing at statistical analysis, so having indices that speed up those operations is encouraged. 23 * 24 * This is it's own separate physical database, so it cannot do joins or queries with any other tables. 25 */ 26class LocalMetricsDatabase private constructor( 27 application: Application, 28 databaseSecret: DatabaseSecret 29) : 30 SQLiteOpenHelper( 31 application, 32 DATABASE_NAME, 33 databaseSecret.asString(), 34 null, 35 DATABASE_VERSION, 36 0, 37 SqlCipherDeletingErrorHandler(DATABASE_NAME), 38 SqlCipherDatabaseHook(), 39 true 40 ), 41 SignalDatabaseOpenHelper { 42 43 companion object { 44 private val TAG = Log.tag(LocalMetricsDatabase::class.java) 45 46 private val MAX_AGE = TimeUnit.DAYS.toMillis(7) 47 48 private const val DATABASE_VERSION = 1 49 private const val DATABASE_NAME = "signal-local-metrics.db" 50 51 private const val TABLE_NAME = "events" 52 private const val ID = "_id" 53 private const val CREATED_AT = "created_at" 54 private const val EVENT_ID = "event_id" 55 private const val EVENT_NAME = "event_name" 56 private const val SPLIT_NAME = "split_name" 57 private const val DURATION = "duration" 58 59 private val CREATE_TABLE = """ 60 CREATE TABLE $TABLE_NAME ( 61 $ID INTEGER PRIMARY KEY, 62 $CREATED_AT INTEGER NOT NULL, 63 $EVENT_ID TEXT NOT NULL, 64 $EVENT_NAME TEXT NOT NULL, 65 $SPLIT_NAME TEXT NOT NULL, 66 $DURATION INTEGER NOT NULL 67 ) 68 """ 69 70 private val CREATE_INDEXES = arrayOf( 71 "CREATE INDEX events_create_at_index ON $TABLE_NAME ($CREATED_AT)", 72 "CREATE INDEX events_event_name_split_name_index ON $TABLE_NAME ($EVENT_NAME, $SPLIT_NAME)", 73 "CREATE INDEX events_duration_index ON $TABLE_NAME ($DURATION)" 74 ) 75 76 @SuppressLint("StaticFieldLeak") // We hold an Application context, not a view context 77 @Volatile 78 private var instance: LocalMetricsDatabase? = null 79 80 @JvmStatic 81 fun getInstance(context: Application): LocalMetricsDatabase { 82 if (instance == null) { 83 synchronized(LocalMetricsDatabase::class.java) { 84 if (instance == null) { 85 SqlCipherLibraryLoader.load() 86 instance = LocalMetricsDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context)) 87 } 88 } 89 } 90 return instance!! 91 } 92 } 93 94 private object EventTotals { 95 const val VIEW_NAME = "event_totals" 96 97 val CREATE_VIEW = """ 98 CREATE VIEW $VIEW_NAME AS 99 SELECT $EVENT_ID, $EVENT_NAME, SUM($DURATION) AS $DURATION 100 FROM $TABLE_NAME 101 GROUP BY $EVENT_ID 102 """ 103 } 104 105 override fun onCreate(db: SQLiteDatabase) { 106 Log.i(TAG, "onCreate()") 107 108 db.execSQL(CREATE_TABLE) 109 CREATE_INDEXES.forEach { db.execSQL(it) } 110 111 db.execSQL(EventTotals.CREATE_VIEW) 112 } 113 114 override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 115 } 116 117 override fun onOpen(db: SQLiteDatabase) { 118 db.setForeignKeyConstraintsEnabled(true) 119 } 120 121 override fun getSqlCipherDatabase(): SQLiteDatabase { 122 return writableDatabase 123 } 124 125 fun insert(currentTime: Long, event: LocalMetricsEvent) { 126 val db = writableDatabase 127 128 db.beginTransaction() 129 try { 130 event.splits.forEach { split -> 131 db.insert( 132 TABLE_NAME, 133 null, 134 ContentValues().apply { 135 put(CREATED_AT, event.createdAt) 136 put(EVENT_ID, event.eventId) 137 put(EVENT_NAME, event.eventName) 138 put(SPLIT_NAME, split.name) 139 put(DURATION, event.timeunit.convert(split.duration, TimeUnit.NANOSECONDS)) 140 } 141 ) 142 } 143 144 db.delete(TABLE_NAME, "$CREATED_AT < ?", SqlUtil.buildArgs(currentTime - MAX_AGE)) 145 146 db.setTransactionSuccessful() 147 } finally { 148 db.endTransaction() 149 } 150 } 151 152 fun clear() { 153 writableDatabase.delete(TABLE_NAME, null, null) 154 } 155 156 fun getOldestMetricTime(eventName: String): Long { 157 readableDatabase.rawQuery("SELECT $CREATED_AT FROM $TABLE_NAME WHERE $EVENT_NAME = ? ORDER BY $CREATED_AT ASC", SqlUtil.buildArgs(eventName)).use { cursor -> 158 return if (cursor.moveToFirst()) { 159 cursor.getLong(0) 160 } else { 161 0 162 } 163 } 164 } 165 166 fun getMetrics(): List<EventMetrics> { 167 val db = readableDatabase 168 169 db.beginTransaction() 170 try { 171 val events: Map<String, List<String>> = getUniqueEventNames() 172 173 val metrics: List<EventMetrics> = events.map { (eventName: String, splits: List<String>) -> 174 EventMetrics( 175 name = eventName, 176 count = getCount(eventName), 177 p50 = eventPercent(eventName, 50), 178 p90 = eventPercent(eventName, 90), 179 p99 = eventPercent(eventName, 99), 180 splits = splits.map { splitName -> 181 SplitMetrics( 182 name = splitName, 183 p50 = splitPercent(eventName, splitName, 50), 184 p90 = splitPercent(eventName, splitName, 90), 185 p99 = splitPercent(eventName, splitName, 99) 186 ) 187 } 188 ) 189 } 190 191 db.setTransactionSuccessful() 192 193 return metrics 194 } finally { 195 db.endTransaction() 196 } 197 } 198 199 private fun getUniqueEventNames(): Map<String, List<String>> { 200 val events = mutableMapOf<String, MutableList<String>>() 201 202 readableDatabase.rawQuery("SELECT DISTINCT $EVENT_NAME, $SPLIT_NAME FROM $TABLE_NAME", null).use { cursor -> 203 while (cursor.moveToNext()) { 204 val eventName = CursorUtil.requireString(cursor, EVENT_NAME) 205 val splitName = CursorUtil.requireString(cursor, SPLIT_NAME) 206 207 events.getOrPut(eventName) { 208 mutableListOf() 209 }.add(splitName) 210 } 211 } 212 213 return events 214 } 215 216 private fun getCount(eventName: String): Long { 217 readableDatabase.rawQuery("SELECT COUNT(DISTINCT $EVENT_ID) FROM $TABLE_NAME WHERE $EVENT_NAME = ?", SqlUtil.buildArgs(eventName)).use { cursor -> 218 return if (cursor.moveToFirst()) { 219 cursor.getLong(0) 220 } else { 221 0 222 } 223 } 224 } 225 226 fun eventPercent(eventName: String, percent: Int): Long { 227 return percentile(EventTotals.VIEW_NAME, "$EVENT_NAME = '$eventName'", percent) 228 } 229 230 private fun splitPercent(eventName: String, splitName: String, percent: Int): Long { 231 return percentile(TABLE_NAME, "$EVENT_NAME = '$eventName' AND $SPLIT_NAME = '$splitName'", percent) 232 } 233 234 private fun percentile(table: String, where: String, percent: Int): Long { 235 val query: String = """ 236 SELECT $DURATION 237 FROM $table 238 WHERE $where 239 ORDER BY $DURATION ASC 240 LIMIT 1 241 OFFSET (SELECT COUNT(*) 242 FROM $table 243 WHERE $where) * $percent / 100 - 1 244 """ 245 246 readableDatabase.rawQuery(query, null).use { cursor -> 247 return if (cursor.moveToFirst()) { 248 cursor.getLong(0) 249 } else { 250 -1 251 } 252 } 253 } 254 255 data class EventMetrics( 256 val name: String, 257 val count: Long, 258 val p50: Long, 259 val p90: Long, 260 val p99: Long, 261 val splits: List<SplitMetrics> 262 ) 263 264 data class SplitMetrics( 265 val name: String, 266 val p50: Long, 267 val p90: Long, 268 val p99: Long 269 ) 270}