1
0
Fork 0

reduced the lag when filtering is on and typing or pressing Backspace quickly, by killing the database query and providing basic 'a,b,c' suggestions

This commit is contained in:
sspanak 2024-10-25 12:37:21 +03:00 committed by Dimo Karaivanov
parent 20c78e47f4
commit 7b37538899
4 changed files with 84 additions and 28 deletions

View file

@ -1,22 +1,35 @@
package io.github.sspanak.tt9.db; package io.github.sspanak.tt9.db;
import android.content.Context; import android.content.Context;
import android.os.CancellationSignal;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import io.github.sspanak.tt9.db.entities.AddWordResult; import io.github.sspanak.tt9.db.entities.AddWordResult;
import io.github.sspanak.tt9.db.wordPairs.WordPairStore; import io.github.sspanak.tt9.db.wordPairs.WordPairStore;
import io.github.sspanak.tt9.db.words.DictionaryLoader; import io.github.sspanak.tt9.db.words.DictionaryLoader;
import io.github.sspanak.tt9.db.words.WordStore; import io.github.sspanak.tt9.db.words.WordStore;
import io.github.sspanak.tt9.languages.Language; import io.github.sspanak.tt9.languages.Language;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.ConsumerCompat; import io.github.sspanak.tt9.util.ConsumerCompat;
import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.Logger;
public class DataStore { public class DataStore {
private static final Handler asyncHandler = new Handler(); private final static String LOG_TAG = DataStore.class.getSimpleName();
private static final Handler asyncReturn = new Handler();
private static final ExecutorService executor = Executors.newCachedThreadPool();
private static Future<?> getWordsTask;
private static CancellationSignal getWordsCancellationSignal = new CancellationSignal();
private static WordPairStore pairs; private static WordPairStore pairs;
private static WordStore words; private static WordStore words;
@ -28,7 +41,7 @@ public class DataStore {
private static void runInThread(@NonNull Runnable action) { private static void runInThread(@NonNull Runnable action) {
new Thread(action).start(); executor.submit(action);
} }
@ -40,7 +53,7 @@ public class DataStore {
words.finishTransaction(); words.finishTransaction();
} catch (Exception e) { } catch (Exception e) {
words.failTransaction(); words.failTransaction();
Logger.e(DataStore.class.getSimpleName(), errorMessagePrefix + " " + e.getMessage()); Logger.e(LOG_TAG, errorMessagePrefix + " " + e.getMessage());
} }
onFinish.run(); onFinish.run();
}); });
@ -85,17 +98,42 @@ public class DataStore {
public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) { public static void getWords(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) {
runInThread(() -> { if (getWordsTask != null && !getWordsTask.isDone()) {
ArrayList<String> data = words.getSimilar(language, sequence, filter, minWords, maxWords); dataHandler.accept(new ArrayList<>());
asyncHandler.post(() -> dataHandler.accept(data)); getWordsCancellationSignal.cancel();
}); return;
}
getWordsCancellationSignal = new CancellationSignal();
getWordsTask = executor.submit(() -> getWordsSync(dataHandler, language, sequence, filter, minWords, maxWords));
executor.submit(DataStore::setGetWordsTimeout);
}
private static void getWordsSync(ConsumerCompat<ArrayList<String>> dataHandler, Language language, String sequence, String filter, int minWords, int maxWords) {
try {
ArrayList<String> data = words.getSimilar(getWordsCancellationSignal, language, sequence, filter, minWords, maxWords);
asyncReturn.post(() -> dataHandler.accept(data));
} catch (Exception e) {
Logger.e(LOG_TAG, "Error fetching words: " + e.getMessage());
}
}
private static void setGetWordsTimeout() {
try {
getWordsTask.get(SettingsStore.SLOW_QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (Exception e) {
getWordsCancellationSignal.cancel();
Logger.e(LOG_TAG, "Word loading timed out after " + SettingsStore.SLOW_QUERY_TIMEOUT + " ms.");
}
} }
public static void getCustomWords(ConsumerCompat<ArrayList<String>> dataHandler, String wordFilter, int maxWords) { public static void getCustomWords(ConsumerCompat<ArrayList<String>> dataHandler, String wordFilter, int maxWords) {
runInThread(() -> { runInThread(() -> {
ArrayList<String> data = words.getSimilarCustom(wordFilter, maxWords); ArrayList<String> data = words.getSimilarCustom(wordFilter, maxWords);
asyncHandler.post(() -> dataHandler.accept(data)); asyncReturn.post(() -> dataHandler.accept(data));
}); });
} }
@ -103,7 +141,7 @@ public class DataStore {
public static void countCustomWords(ConsumerCompat<Long> dataHandler) { public static void countCustomWords(ConsumerCompat<Long> dataHandler) {
runInThread(() -> { runInThread(() -> {
long data = words.countCustom(); long data = words.countCustom();
asyncHandler.post(() -> dataHandler.accept(data)); asyncReturn.post(() -> dataHandler.accept(data));
}); });
} }
@ -111,7 +149,7 @@ public class DataStore {
public static void exists(ConsumerCompat<ArrayList<Integer>> dataHandler, ArrayList<Language> languages) { public static void exists(ConsumerCompat<ArrayList<Integer>> dataHandler, ArrayList<Language> languages) {
runInThread(() -> { runInThread(() -> {
ArrayList<Integer> data = words.exists(languages); ArrayList<Integer> data = words.exists(languages);
asyncHandler.post(() -> dataHandler.accept(data)); asyncReturn.post(() -> dataHandler.accept(data));
}); });
} }

View file

@ -4,6 +4,8 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteDoneException;
import android.database.sqlite.SQLiteStatement; import android.database.sqlite.SQLiteStatement;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -118,19 +120,19 @@ public class ReadOps {
@NonNull @NonNull
public WordList getWords(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) { public WordList getWords(@NonNull SQLiteDatabase db, CancellationSignal cancel, @NonNull Language language, @NonNull String positions, String filter, int maximumWords, boolean fullOutput) {
if (positions.isEmpty()) { if (positions.isEmpty()) {
Logger.d(LOG_TAG, "No word positions. Not searching words."); Logger.d(LOG_TAG, "No word positions. Not searching words.");
return new WordList(); return new WordList();
} }
String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput); String wordsQuery = getWordsQuery(language, positions, filter, maximumWords, fullOutput);
if (wordsQuery.isEmpty()) { if (wordsQuery.isEmpty() || cancel.isCanceled()) {
return new WordList(); return new WordList();
} }
WordList words = new WordList(); WordList words = new WordList();
try (Cursor cursor = db.rawQuery(wordsQuery, null)) { try (Cursor cursor = db.rawQuery(wordsQuery, null, cancel)) {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
words.add( words.add(
cursor.getString(0), cursor.getString(0),
@ -138,26 +140,29 @@ public class ReadOps {
fullOutput ? cursor.getInt(2) : 0 fullOutput ? cursor.getInt(2) : 0
); );
} }
} catch (OperationCanceledException e) {
Logger.d(LOG_TAG, "Words query cancelled!");
return words;
} }
return words; return words;
} }
public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) { public String getSimilarWordPositions(@NonNull SQLiteDatabase db, @NonNull CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, String wordFilter, int minPositions) {
int generations = switch (sequence.length()) { int generations = switch (sequence.length()) {
case 2 -> wordFilter.isEmpty() ? 1 : 10; case 2 -> wordFilter.isEmpty() ? 1 : 10;
case 3, 4 -> wordFilter.isEmpty() ? 2 : 10; case 3, 4 -> wordFilter.isEmpty() ? 2 : 10;
default -> 10; default -> 10;
}; };
return getWordPositions(db, language, sequence, generations, minPositions, wordFilter); return getWordPositions(db, cancel, language, sequence, generations, minPositions, wordFilter);
} }
@NonNull @NonNull
public String getWordPositions(@NonNull SQLiteDatabase db, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) { public String getWordPositions(@NonNull SQLiteDatabase db, CancellationSignal cancel, @NonNull Language language, @NonNull String sequence, int generations, int minPositions, String wordFilter) {
if (sequence.length() == 1) { if (sequence.length() == 1 || cancel.isCanceled()) {
return sequence; return sequence;
} }
@ -165,18 +170,24 @@ public class ReadOps {
String cachedFactoryPositions = SlowQueryStats.getCachedIfSlow(SlowQueryStats.generateKey(language, sequence, wordFilter, minPositions)); String cachedFactoryPositions = SlowQueryStats.getCachedIfSlow(SlowQueryStats.generateKey(language, sequence, wordFilter, minPositions));
if (cachedFactoryPositions != null) { if (cachedFactoryPositions != null) {
String customWordPositions = getCustomWordPositions(db, language, sequence, generations); String customWordPositions = getCustomWordPositions(db, cancel, language, sequence, generations);
return customWordPositions.isEmpty() ? cachedFactoryPositions : customWordPositions + "," + cachedFactoryPositions; return customWordPositions.isEmpty() ? cachedFactoryPositions : customWordPositions + "," + cachedFactoryPositions;
} }
try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null)) { try (Cursor cursor = db.rawQuery(getPositionsQuery(language, sequence, generations), null, cancel)) {
positions.appendFromDbRanges(cursor); positions.appendFromDbRanges(cursor);
} catch (OperationCanceledException ignored) {
Logger.d(LOG_TAG, "Word positions query cancelled!");
return sequence;
} }
if (positions.size < minPositions && generations < Integer.MAX_VALUE) { if (positions.size < minPositions && generations < Integer.MAX_VALUE) {
Logger.d(LOG_TAG, "Not enough positions: " + positions.size + " < " + minPositions + ". Searching for more."); Logger.d(LOG_TAG, "Not enough positions: " + positions.size + " < " + minPositions + ". Searching for more.");
try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null)) { try (Cursor cursor = db.rawQuery(getFactoryWordPositionsQuery(language, sequence, Integer.MAX_VALUE), null, cancel)) {
positions.appendFromDbRanges(cursor); positions.appendFromDbRanges(cursor);
} catch (OperationCanceledException ignored) {
Logger.d(LOG_TAG, "Word positions query cancelled!");
return sequence;
} }
} }
@ -184,9 +195,12 @@ public class ReadOps {
} }
@NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, Language language, String sequence, int generations) { @NonNull private String getCustomWordPositions(@NonNull SQLiteDatabase db, CancellationSignal cancel, Language language, String sequence, int generations) {
try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null)) { try (Cursor cursor = db.rawQuery(getCustomWordPositionsQuery(language, sequence, generations), null, cancel)) {
return new WordPositionsStringBuilder().appendFromDbRanges(cursor).toString(); return new WordPositionsStringBuilder().appendFromDbRanges(cursor).toString();
} catch (OperationCanceledException e) {
Logger.d(LOG_TAG, "Custom word positions query cancelled.");
return "";
} }
} }

View file

@ -1,6 +1,7 @@
package io.github.sspanak.tt9.db.words; package io.github.sspanak.tt9.db.words;
import android.content.Context; import android.content.Context;
import android.os.CancellationSignal;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -60,7 +61,7 @@ public class WordStore extends BaseSyncStore {
* For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ... * For example: "7655" -> "roll" (exact match), but also: "rolled", "roller", "rolling", ...
* and other similar. * and other similar.
*/ */
public ArrayList<String> getSimilar(Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) { public ArrayList<String> getSimilar(@NonNull CancellationSignal cancel, Language language, String sequence, String wordFilter, int minimumWords, int maximumWords) {
if (!checkOrNotify()) { if (!checkOrNotify()) {
return new ArrayList<>(); return new ArrayList<>();
} }
@ -80,15 +81,17 @@ public class WordStore extends BaseSyncStore {
final String filter = wordFilter == null ? "" : wordFilter; final String filter = wordFilter == null ? "" : wordFilter;
Timer.start("get_positions"); Timer.start("get_positions");
String positions = readOps.getSimilarWordPositions(sqlite.getDb(), language, sequence, filter, minWords); String positions = readOps.getSimilarWordPositions(sqlite.getDb(), cancel, language, sequence, filter, minWords);
long positionsTime = Timer.stop("get_positions"); long positionsTime = Timer.stop("get_positions");
Timer.start("get_words"); Timer.start("get_words");
ArrayList<String> words = readOps.getWords(sqlite.getDb(), language, positions, filter, maxWords, false).toStringList(); ArrayList<String> words = readOps.getWords(sqlite.getDb(), cancel, language, positions, filter, maxWords, false).toStringList();
long wordsTime = Timer.stop("get_words"); long wordsTime = Timer.stop("get_words");
printLoadingSummary(sequence, words, positionsTime, wordsTime); printLoadingSummary(sequence, words, positionsTime, wordsTime);
if (!cancel.isCanceled()) { // do not store empty results from aborted queries in the cache
SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions); SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions);
}
return words; return words;
} }
@ -188,8 +191,8 @@ public class WordStore extends BaseSyncStore {
try { try {
Timer.start(LOG_TAG); Timer.start(LOG_TAG);
String topWordPositions = readOps.getWordPositions(sqlite.getDb(), language, sequence, 0, 0, ""); String topWordPositions = readOps.getWordPositions(sqlite.getDb(), null, language, sequence, 0, 0, "");
WordList topWords = readOps.getWords(sqlite.getDb(), language, topWordPositions, "", 9999, true); WordList topWords = readOps.getWords(sqlite.getDb(), null, language, topWordPositions, "", 9999, true);
if (topWords.isEmpty()) { if (topWords.isEmpty()) {
throw new Exception("No such word"); throw new Exception("No such word");
} }

View file

@ -20,6 +20,7 @@ public class SettingsStore extends SettingsUI {
public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms public final static int DICTIONARY_IMPORT_PROGRESS_UPDATE_TIME = 250; // ms
public final static int RESIZE_THROTTLING_TIME = 60; // ms public final static int RESIZE_THROTTLING_TIME = 60; // ms
public final static byte SLOW_QUERY_TIME = 50; // ms public final static byte SLOW_QUERY_TIME = 50; // ms
public final static int SLOW_QUERY_TIMEOUT = 3000; // ms
public final static int SOFT_KEY_DOUBLE_CLICK_DELAY = 500; // ms public final static int SOFT_KEY_DOUBLE_CLICK_DELAY = 500; // ms
public final static int SOFT_KEY_REPEAT_DELAY = 40; // ms public final static int SOFT_KEY_REPEAT_DELAY = 40; // ms
public final static int SOFT_KEY_TITLE_MAX_CHARS = 5; public final static int SOFT_KEY_TITLE_MAX_CHARS = 5;