That fuck shit the fascists are using
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}