From bc54f3f916cd1cb06990b0970504180fc6b08b6c Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 1 Apr 2016 16:21:22 -0400 Subject: [PATCH] Improve performance of toggleCheckmark --- .../unit/models/RepetitionListTest.java | 14 ++++ .../uhabits/helpers/DatabaseHelper.java | 18 +++++ .../isoron/uhabits/models/CheckmarkList.java | 47 +++++++----- .../isoron/uhabits/models/RepetitionList.java | 32 ++++++--- .../org/isoron/uhabits/models/ScoreList.java | 71 ++++++++++++------- 5 files changed, 133 insertions(+), 49 deletions(-) diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java index bb3c0887b..a15841faa 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java @@ -25,6 +25,7 @@ import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseTest; import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.unit.HabitFixtures; import org.junit.After; import org.junit.Before; @@ -37,6 +38,7 @@ import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Random; +import static junit.framework.Assert.assertFalse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -174,4 +176,16 @@ public class RepetitionListTest extends BaseTest from = to - 5 * DateHelper.millisecondsInOneDay; assertThat(habit.repetitions.count(from, to), equalTo(3)); } + + @Test + public void getOldest() + { + long expectedOldestTimestamp = DateHelper.getStartOfToday() - 9 * DateHelper.millisecondsInOneDay; + + assertThat(habit.repetitions.getOldestTimestamp(), equalTo(expectedOldestTimestamp)); + + Repetition oldest = habit.repetitions.getOldest(); + assertFalse(oldest == null); + assertThat(oldest.timestamp, equalTo(expectedOldestTimestamp)); + } } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 03f66ec5a..fa8c509b6 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -20,11 +20,13 @@ package org.isoron.uhabits.helpers; import android.content.Context; +import android.database.Cursor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import com.activeandroid.ActiveAndroid; +import com.activeandroid.Cache; import com.activeandroid.Configuration; import org.isoron.uhabits.BuildConfig; @@ -163,4 +165,20 @@ public class DatabaseHelper ActiveAndroid.initialize(dbConfig); } + + public static long longQuery(String query, String args[]) + { + Cursor c = null; + + try + { + c = Cache.openDatabase().rawQuery(query, args); + if (!c.moveToFirst()) return 0; + return c.getLong(0); + } + finally + { + if(c != null) c.close(); + } + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 8c5f5f577..f761f26c9 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -21,10 +21,10 @@ package org.isoron.uhabits.models; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.activeandroid.ActiveAndroid; import com.activeandroid.Cache; import com.activeandroid.query.Delete; import com.activeandroid.query.Select; @@ -134,10 +134,9 @@ public class CheckmarkList */ protected void computeAll() { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return; + long fromTimestamp = habit.repetitions.getOldestTimestamp(); + if(fromTimestamp == 0) return; - Long fromTimestamp = oldestRep.timestamp; Long toTimestamp = DateHelper.getStartOfToday(); compute(fromTimestamp, toTimestamp); @@ -150,9 +149,9 @@ public class CheckmarkList * @param from timestamp for the beginning of the interval * @param to timestamp for the end of the interval */ - protected void compute(long from, long to) + protected void compute(long from, final long to) { - long day = DateHelper.millisecondsInOneDay; + final long day = DateHelper.millisecondsInOneDay; Checkmark newestCheckmark = findNewest(); if(newestCheckmark != null) @@ -165,9 +164,9 @@ public class CheckmarkList .selectFromTo(fromExtended, to) .execute(); - int nDays = (int) ((to - from) / day) + 1; + final int nDays = (int) ((to - from) / day) + 1; int nDaysExtended = (int) ((to - fromExtended) / day) + 1; - int checks[] = new int[nDaysExtended]; + final int checks[] = new int[nDaysExtended]; for (Repetition rep : reps) { @@ -187,24 +186,38 @@ public class CheckmarkList checks[i] = Checkmark.CHECKED_IMPLICITLY; } - ActiveAndroid.beginTransaction(); + + long timestamps[] = new long[nDays]; + for (int i = 0; i < nDays; i++) + timestamps[i] = to - i * day; + + insert(timestamps, checks); + } + + private void insert(long timestamps[], int values[]) + { + String query = "insert into Checkmarks(habit, timestamp, value) values (?,?,?)"; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); try { - for (int i = 0; i < nDays; i++) + SQLiteStatement statement = db.compileStatement(query); + statement.bindString(1, habit.getId().toString()); + + for (int i = 0; i < timestamps.length; i++) { - Checkmark c = new Checkmark(); - c.habit = habit; - c.timestamp = to - i * day; - c.value = checks[i]; - c.save(); + statement.bindLong(2, timestamps[i]); + statement.bindLong(3, values[i]); + statement.execute(); } - ActiveAndroid.setTransactionSuccessful(); + db.setTransactionSuccessful(); } finally { - ActiveAndroid.endTransaction(); + db.endTransaction(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index 6e6d720ed..5bfe22fba 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -28,7 +28,9 @@ import com.activeandroid.Cache; import com.activeandroid.query.Delete; import com.activeandroid.query.From; import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; +import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.helpers.DateHelper; import java.util.Arrays; @@ -96,22 +98,21 @@ public class RepetitionList timestamp = DateHelper.getStartOfDay(timestamp); if (contains(timestamp)) - { delete(timestamp); - } else - { - Repetition rep = new Repetition(); - rep.habit = habit; - rep.timestamp = timestamp; - rep.save(); - } + insert(timestamp); habit.scores.invalidateNewerThan(timestamp); habit.checkmarks.deleteNewerThan(timestamp); habit.streaks.deleteNewerThan(timestamp); } + private void insert(long timestamp) + { + String[] args = { habit.getId().toString(), Long.toString(timestamp) }; + SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args); + } + /** * Returns the oldest repetition for the habit. If there is no repetition, returns null. * Repetitions in the future are discarded. @@ -124,6 +125,21 @@ public class RepetitionList return (Repetition) select().limit(1).executeSingle(); } + /** + * Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero. + * Repetitions in the future are discarded. + * + * @return timestamp of the oldest repetition + */ + public long getOldestTimestamp() + { + String[] args = { habit.getId().toString(), Long.toString(DateHelper.getStartOfToday()) }; + String query = "select timestamp from Repetitions where habit = ? and timestamp <= ? " + + "order by timestamp limit 1"; + + return DatabaseHelper.longQuery(query, args); + } + /** * Returns the total number of repetitions for each month, from the first repetition until * today, grouped by day of week. The repetitions are returned in a HashMap. The key is the diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index 1972dc411..36905ebd6 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -21,6 +21,7 @@ package org.isoron.uhabits.models; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -28,8 +29,8 @@ import com.activeandroid.Cache; import com.activeandroid.query.Delete; import com.activeandroid.query.From; import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; -import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.helpers.DateHelper; import java.io.IOException; @@ -104,10 +105,9 @@ public class ScoreList */ private void computeAll() { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return; + long fromTimestamp = habit.repetitions.getOldestTimestamp(); + if(fromTimestamp == 0) return; - long fromTimestamp = oldestRep.timestamp; long toTimestamp = DateHelper.getStartOfToday(); compute(fromTimestamp, toTimestamp); } @@ -136,28 +136,51 @@ public class ScoreList from = newestScore.timestamp + day; final int checkmarkValues[] = habit.checkmarks.getValues(from, to); - final int firstScore = newestScoreValue; final long beginning = from; - DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() + + int lastScore = newestScoreValue; + int size = checkmarkValues.length; + + long timestamps[] = new long[size]; + long values[] = new long[size]; + + for (int i = 0; i < checkmarkValues.length; i++) + { + int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1]; + lastScore = Score.compute(freq, lastScore, checkmarkValue); + timestamps[i] = beginning + day * i; + values[i] = lastScore; + } + + insert(timestamps, values); + } + + private void insert(long timestamps[], long values[]) + { + String query = "insert into Score(habit, timestamp, score) values (?,?,?)"; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); + + try { - @Override - public void execute() + SQLiteStatement statement = db.compileStatement(query); + statement.bindString(1, habit.getId().toString()); + + for (int i = 0; i < timestamps.length; i++) { - int lastScore = firstScore; - - for (int i = 0; i < checkmarkValues.length; i++) - { - int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1]; - - Score s = new Score(); - s.habit = habit; - s.timestamp = beginning + day * i; - s.score = lastScore = Score.compute(freq, lastScore, checkmarkValue); - s.save(); - } + statement.bindLong(2, timestamps[i]); + statement.bindLong(3, values[i]); + statement.execute(); } - }); + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } } /** @@ -185,9 +208,9 @@ public class ScoreList */ public int getValue(long timestamp) { - Score s = get(timestamp); - if(s == null) return 0; - else return s.score; + computeAll(); + String[] args = { habit.getId().toString(), Long.toString(timestamp) }; + return SQLiteUtils.intQuery("select score from Score where habit = ? and timestamp = ?", args); } /**