blob: b7460cd6ead453f40382eb56c9a1ef94ab95a3fc [file] [log] [blame]
Aurimas Liutikas88c7ff12023-08-10 12:42:26 -07001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package android.database;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertTrue;
21
22import android.content.ContentValues;
23import android.content.Context;
24import android.database.sqlite.SQLiteDatabase;
25import android.perftests.utils.BenchmarkState;
26import android.perftests.utils.PerfStatusReporter;
27import android.util.Log;
28import androidx.test.InstrumentationRegistry;
29import androidx.test.filters.LargeTest;
30import androidx.test.runner.AndroidJUnit4;
31import java.io.File;
32import java.util.ArrayList;
33import java.util.Random;
34import org.junit.After;
35import org.junit.Before;
36import org.junit.Rule;
37import org.junit.Test;
38import org.junit.runner.RunWith;
39
40/**
41 * Performance tests for typical CRUD operations and loading rows into the Cursor
42 *
43 * <p>To run: bit CorePerfTests:android.database.SQLiteDatabasePerfTest
44 */
45@RunWith(AndroidJUnit4.class)
46@LargeTest
47public class SQLiteDatabasePerfTest {
48 // TODO b/64262688 Add Concurrency tests to compare WAL vs DELETE read/write
49 private static final String DB_NAME = "dbperftest";
50 private static final int DEFAULT_DATASET_SIZE = 1000;
51
52 @Rule
53 public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
54 private SQLiteDatabase mDatabase;
55 private Context mContext;
56
57 @Before
58 public void setUp() {
59 mContext = InstrumentationRegistry.getTargetContext();
60 mContext.deleteDatabase(DB_NAME);
61
62 createOrOpenTestDatabase(
63 SQLiteDatabase.JOURNAL_MODE_TRUNCATE, SQLiteDatabase.SYNC_MODE_FULL);
64 }
65
66 @After
67 public void tearDown() {
68 mDatabase.close();
69 mContext.deleteDatabase(DB_NAME);
70 }
71
72 private void createOrOpenTestDatabase(String journalMode, String syncMode) {
73 SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder();
74 File dbFile = mContext.getDatabasePath(DB_NAME);
75 if (journalMode != null) {
76 paramsBuilder.setJournalMode(journalMode);
77 }
78 if (syncMode != null) {
79 paramsBuilder.setSynchronousMode(syncMode);
80 }
81 paramsBuilder.addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY);
82
83 mDatabase = SQLiteDatabase.openDatabase(dbFile, paramsBuilder.build());
84 mDatabase.execSQL("CREATE TABLE T1 "
85 + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)");
86 mDatabase.execSQL("CREATE TABLE T2 ("
87 + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER,"
88 + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))");
89 }
90
91 @Test
92 public void testSelect() {
93 insertT1TestDataSet();
94
95 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
96
97 Random rnd = new Random(0);
98 while (state.keepRunning()) {
99 int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
100 try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
101 + "WHERE _ID=?", new String[]{String.valueOf(index)})) {
102 assertTrue(cursor.moveToNext());
103 assertEquals(index, cursor.getInt(0));
104 assertEquals(index, cursor.getInt(1));
105 assertEquals("T1Value" + index, cursor.getString(2));
106 assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d);
107 }
108 }
109 }
110
111 @Test
112 public void testSelectCacheMissRate() {
113 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
114
115 insertT1TestDataSet();
116
117 ArrayList<String> queryPool = new ArrayList<>();
118 queryPool.add("SELECT _ID, COL_A, COL_B, COL_C FROM T1 WHERE _ID=?");
119 queryPool.add("SELECT _ID FROM T1 WHERE _ID=?");
120 queryPool.add("SELECT COL_A FROM T1 WHERE _ID=?");
121 queryPool.add("SELECT COL_B FROM T1 WHERE _ID=?");
122 queryPool.add("SELECT COL_C FROM T1 WHERE _ID=?");
123 queryPool.add("SELECT _ID, COL_A FROM T1 WHERE _ID=?");
124 queryPool.add("SELECT _ID, COL_B FROM T1 WHERE _ID=?");
125 queryPool.add("SELECT _ID, COL_C FROM T1 WHERE _ID=?");
126 queryPool.add("SELECT COL_A, COL_B FROM T1 WHERE _ID=?");
127 queryPool.add("SELECT COL_A, COL_C FROM T1 WHERE _ID=?");
128 queryPool.add("SELECT COL_B, COL_C FROM T1 WHERE _ID=?");
129 while (state.keepRunning()) {
130 Random rnd = new Random(0);
131
132 int queries = 1000;
133 for (int iQuery = 0; iQuery < queries; ++iQuery) {
134 int queryIndex = rnd.nextInt(queryPool.size());
135 int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
136
137 try (Cursor cursor = mDatabase.rawQuery(
138 queryPool.get(queryIndex), new String[] {String.valueOf(index)})) {
139 assertTrue(cursor.moveToNext());
140 }
141 }
142 }
143
144 Log.d("testSelectMemory",
145 "cacheMissRate: " + mDatabase.getStatementCacheMissRate()
146 + "Total Statements: " + mDatabase.getTotalPreparedStatements()
147 + ". Misses: " + mDatabase.getTotalStatementCacheMisses());
148
149 // Make sure caching is working and our miss rate should definitely be less than 100%
150 // however, we would expect this number to be actually closer to 0.
151 assertTrue(mDatabase.getStatementCacheMissRate() < 1);
152 mDatabase.close();
153 mContext.deleteDatabase(DB_NAME);
154 }
155
156 @Test
157 public void testSelectMultipleRows() {
158 insertT1TestDataSet();
159
160 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
161 Random rnd = new Random(0);
162 final int querySize = 50;
163 while (state.keepRunning()) {
164 int index = rnd.nextInt(DEFAULT_DATASET_SIZE - querySize - 1);
165 try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
166 + "WHERE _ID BETWEEN ? and ? ORDER BY _ID",
167 new String[]{String.valueOf(index), String.valueOf(index + querySize - 1)})) {
168 int i = 0;
169 while(cursor.moveToNext()) {
170 assertEquals(index, cursor.getInt(0));
171 assertEquals(index, cursor.getInt(1));
172 assertEquals("T1Value" + index, cursor.getString(2));
173 assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d);
174 index++;
175 i++;
176 }
177 assertEquals(querySize, i);
178 }
179 }
180 }
181
182 @Test
183 public void testCursorIterateForward() {
184 // A larger dataset is needed to exceed default CursorWindow size
185 int datasetSize = DEFAULT_DATASET_SIZE * 50;
186 insertT1TestDataSet(datasetSize);
187
188 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
189 while (state.keepRunning()) {
190 try (Cursor cursor = mDatabase
191 .rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 ORDER BY _ID", null)) {
192 int i = 0;
193 while(cursor.moveToNext()) {
194 assertEquals(i, cursor.getInt(0));
195 assertEquals(i, cursor.getInt(1));
196 assertEquals("T1Value" + i, cursor.getString(2));
197 assertEquals(1.1 * i, cursor.getDouble(3), 0.0000001d);
198 i++;
199 }
200 assertEquals(datasetSize, i);
201 }
202 }
203 }
204
205 @Test
206 public void testCursorIterateBackwards() {
207 // A larger dataset is needed to exceed default CursorWindow size
208 int datasetSize = DEFAULT_DATASET_SIZE * 50;
209 insertT1TestDataSet(datasetSize);
210
211 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
212 while (state.keepRunning()) {
213 try (Cursor cursor = mDatabase
214 .rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 ORDER BY _ID", null)) {
215 int i = datasetSize - 1;
216 while(cursor.moveToPosition(i)) {
217 assertEquals(i, cursor.getInt(0));
218 assertEquals(i, cursor.getInt(1));
219 assertEquals("T1Value" + i, cursor.getString(2));
220 assertEquals(1.1 * i, cursor.getDouble(3), 0.0000001d);
221 i--;
222 }
223 assertEquals(-1, i);
224 }
225 }
226 }
227
228 @Test
229 public void testInnerJoin() {
230 mDatabase.setForeignKeyConstraintsEnabled(true);
231 mDatabase.beginTransaction();
232 insertT1TestDataSet();
233 insertT2TestDataSet();
234 mDatabase.setTransactionSuccessful();
235 mDatabase.endTransaction();
236
237 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
238
239 Random rnd = new Random(0);
240 while (state.keepRunning()) {
241 int index = rnd.nextInt(1000);
242 try (Cursor cursor = mDatabase.rawQuery(
243 "SELECT T1._ID, T1.COL_A, T1.COL_B, T1.COL_C, T2.COL_A FROM T1 "
244 + "INNER JOIN T2 on T2.T1_ID=T1._ID WHERE T1._ID = ?",
245 new String[]{String.valueOf(index)})) {
246 assertTrue(cursor.moveToNext());
247 assertEquals(index, cursor.getInt(0));
248 assertEquals(index, cursor.getInt(1));
249 assertEquals("T1Value" + index, cursor.getString(2));
250 assertEquals(1.1 * index, cursor.getDouble(3), 0.0000001d);
251 assertEquals("T2Value" + index, cursor.getString(4));
252 }
253 }
254 }
255
256 /**
257 * This test measures the insertion of a single row into a database using DELETE journal and
258 * synchronous modes.
259 */
260 @Test
261 public void testInsert() {
262 insertT1TestDataSet();
263
264 testInsertInternal("testInsert");
265 }
266
267 @Test
268 public void testInsertWithPersistFull() {
269 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL);
270 insertT1TestDataSet();
271 testInsertInternal("testInsertWithPersistFull");
272 }
273
274 private void testInsertInternal(String traceTag) {
275 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
276
277 ContentValues cv = new ContentValues();
278 cv.put("_ID", DEFAULT_DATASET_SIZE);
279 cv.put("COL_B", "NewValue");
280 cv.put("COL_C", 1.1);
281 String[] deleteArgs = new String[] {String.valueOf(DEFAULT_DATASET_SIZE)};
282
283 while (state.keepRunning()) {
284 android.os.Trace.beginSection(traceTag);
285 assertEquals(DEFAULT_DATASET_SIZE, mDatabase.insert("T1", null, cv));
286 state.pauseTiming();
287 assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs));
288 state.resumeTiming();
289 android.os.Trace.endSection();
290 }
291 }
292
293 /**
294 * This test measures the insertion of a single row into a database using WAL journal mode and
295 * NORMAL synchronous mode.
296 */
297 @Test
298 public void testInsertWithWalNormalMode() {
299 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
300 insertT1TestDataSet();
301
302 testInsertInternal("testInsertWithWalNormalMode");
303 }
304
305 /**
306 * This test measures the insertion of a single row into a database using WAL journal mode and
307 * FULL synchronous mode. The goal is to see the difference between NORMAL vs FULL sync modes.
308 */
309 @Test
310 public void testInsertWithWalFullMode() {
311 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_FULL);
312
313 insertT1TestDataSet();
314
315 testInsertInternal("testInsertWithWalFullMode");
316 }
317
318 /**
319 * This test measures the insertion of a multiple rows in a single transaction using WAL journal
320 * mode and NORMAL synchronous mode.
321 */
322 @Test
323 public void testBulkInsertWithWalNormalMode() {
324 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
325 testBulkInsertInternal("testBulkInsertWithWalNormalMode");
326 }
327
328 @Test
329 public void testBulkInsertWithPersistFull() {
330 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL);
331 testBulkInsertInternal("testBulkInsertWithPersistFull");
332 }
333
334 /**
335 * This test measures the insertion of a multiple rows in a single transaction using TRUNCATE
336 * journal mode and FULL synchronous mode.
337 */
338 @Test
339 public void testBulkInsert() {
340 testBulkInsertInternal("testBulkInsert");
341 }
342
343 private void testBulkInsertInternal(String traceTag) {
344 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
345
346 String[] statements = new String[DEFAULT_DATASET_SIZE];
347 for (int i = 0; i < DEFAULT_DATASET_SIZE; ++i) {
348 statements[i] = "INSERT INTO T1 VALUES (?,?,?,?)";
349 }
350
351 while (state.keepRunning()) {
352 android.os.Trace.beginSection(traceTag);
353 mDatabase.beginTransaction();
354 for (int i = 0; i < DEFAULT_DATASET_SIZE; ++i) {
355 mDatabase.execSQL(statements[i], new Object[] {i, i, "T1Value" + i, i * 1.1});
356 }
357 mDatabase.setTransactionSuccessful();
358 mDatabase.endTransaction();
359 android.os.Trace.endSection();
360
361 state.pauseTiming();
362 mDatabase.execSQL("DELETE FROM T1");
363 state.resumeTiming();
364 }
365 }
366
367 @Test
368 public void testDelete() {
369 insertT1TestDataSet();
370 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
371 String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)};
372 Object[] insertsArgs = new Object[]{DEFAULT_DATASET_SIZE, DEFAULT_DATASET_SIZE,
373 "ValueToDelete", 1.1};
374
375 while (state.keepRunning()) {
376 state.pauseTiming();
377 mDatabase.execSQL("INSERT INTO T1 VALUES (?, ?, ?, ?)", insertsArgs);
378 state.resumeTiming();
379 assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs));
380 }
381 }
382
383 /**
384 * This test measures the update of a random row in a database.
385 */
386 @Test
387 public void testUpdateWithWalNormalMode() {
388 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
389 insertT1TestDataSet();
390 testUpdateInternal("testUpdateWithWalNormalMode");
391 }
392
393 @Test
394 public void testUpdateWithPersistFull() {
395 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL);
396 insertT1TestDataSet();
397 testUpdateInternal("testUpdateWithPersistFull");
398 }
399
400 @Test
401 public void testUpdate() {
402 insertT1TestDataSet();
403 testUpdateInternal("testUpdate");
404 }
405
406 private void testUpdateInternal(String traceTag) {
407 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
408
409 Random rnd = new Random(0);
410 int i = 0;
411 ContentValues cv = new ContentValues();
412 String[] argArray = new String[1];
413 while (state.keepRunning()) {
414 android.os.Trace.beginSection(traceTag);
415 int id = rnd.nextInt(DEFAULT_DATASET_SIZE);
416 cv.put("COL_A", i);
417 cv.put("COL_B", "UpdatedValue");
418 cv.put("COL_C", i);
419 argArray[0] = String.valueOf(id);
420 assertEquals(1, mDatabase.update("T1", cv, "_ID=?", argArray));
421 i++;
422 android.os.Trace.endSection();
423 }
424 }
425
426 /**
427 * This test measures a multi-threaded read-write environment where there are 2 readers and
428 * 1 writer in the database using TRUNCATE journal mode and FULL syncMode.
429 */
430 @Test
431 public void testMultithreadedReadWrite() {
432 insertT1TestDataSet();
433 performMultithreadedReadWriteTest();
434 }
435
436 private void doReadLoop(int totalIterations) {
437 Random rnd = new Random(0);
438 int currentIteration = 0;
439 while (currentIteration < totalIterations) {
440 android.os.Trace.beginSection("ReadDatabase");
441 int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
442 try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
443 + "WHERE _ID=?",
444 new String[] {String.valueOf(index)})) {
445 cursor.moveToNext();
446 cursor.getInt(0);
447 cursor.getInt(1);
448 cursor.getString(2);
449 cursor.getDouble(3);
450 }
451 ++currentIteration;
452 android.os.Trace.endSection();
453 }
454 }
455
456 private void doReadLoop(BenchmarkState state) {
457 Random rnd = new Random(0);
458 while (state.keepRunning()) {
459 android.os.Trace.beginSection("ReadDatabase");
460 int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
461 try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 "
462 + "WHERE _ID=?",
463 new String[] {String.valueOf(index)})) {
464 cursor.moveToNext();
465 cursor.getInt(0);
466 cursor.getInt(1);
467 cursor.getString(2);
468 cursor.getDouble(3);
469 }
470 android.os.Trace.endSection();
471 }
472 }
473
474 private void doUpdateLoop(int totalIterations) {
475 SQLiteDatabase db = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null);
476 Random rnd = new Random(0);
477 int i = 0;
478 ContentValues cv = new ContentValues();
479 String[] argArray = new String[1];
480
481 while (i < totalIterations) {
482 android.os.Trace.beginSection("UpdateDatabase");
483 int id = rnd.nextInt(DEFAULT_DATASET_SIZE);
484 cv.put("COL_A", i);
485 cv.put("COL_B", "UpdatedValue");
486 cv.put("COL_C", i);
487 argArray[0] = String.valueOf(id);
488 db.update("T1", cv, "_ID=?", argArray);
489 i++;
490 android.os.Trace.endSection();
491 }
492 }
493
494 /**
495 * This test measures a multi-threaded read-write environment where there are 2 readers and
496 * 1 writer in the database using WAL journal mode and NORMAL syncMode.
497 */
498 @Test
499 public void testMultithreadedReadWriteWithWalNormal() {
500 recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL);
501 insertT1TestDataSet();
502
503 performMultithreadedReadWriteTest();
504 }
505
506 private void performMultithreadedReadWriteTest() {
507 int totalBGIterations = 10000;
508 // Writer - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations
509 Thread updateThread = new Thread(() -> { doUpdateLoop(totalBGIterations); });
510
511 // Reader 1 - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations
512 Thread readerThread = new Thread(() -> { doReadLoop(totalBGIterations); });
513
514 updateThread.start();
515 readerThread.start();
516
517 // Reader 2
518 BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
519 doReadLoop(state);
520
521 try {
522 updateThread.join();
523 readerThread.join();
524 } catch (Exception e) {
525 }
526 }
527
528 private void insertT1TestDataSet() {
529 insertT1TestDataSet(DEFAULT_DATASET_SIZE);
530 }
531
532 private void insertT1TestDataSet(int size) {
533 mDatabase.beginTransaction();
534 for (int i = 0; i < size; i++) {
535 mDatabase.execSQL("INSERT INTO T1 VALUES (?, ?, ?, ?)",
536 new Object[]{i, i, "T1Value" + i, i * 1.1});
537 }
538 mDatabase.setTransactionSuccessful();
539 mDatabase.endTransaction();
540 }
541
542 private void insertT2TestDataSet() {
543 mDatabase.beginTransaction();
544 for (int i = 0; i < DEFAULT_DATASET_SIZE; i++) {
545 mDatabase.execSQL("INSERT INTO T2 VALUES (?, ?, ?)",
546 new Object[]{i, "T2Value" + i, i});
547 }
548 mDatabase.setTransactionSuccessful();
549 mDatabase.endTransaction();
550 }
551
552 private void recreateTestDatabase(String journalMode, String syncMode) {
553 mDatabase.close();
554 mContext.deleteDatabase(DB_NAME);
555 createOrOpenTestDatabase(journalMode, syncMode);
556 }
557}
558