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:
parent
20c78e47f4
commit
7b37538899
4 changed files with 84 additions and 28 deletions
|
|
@ -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));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
SlowQueryStats.add(SlowQueryStats.generateKey(language, sequence, wordFilter, minWords), (int) (positionsTime + wordsTime), positions);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue