RealmWriteService + FlatTimeInterval for better performance

threading
Laurent 3 years ago
parent 526e50f8e4
commit 929365fc4c
  1. 4
      app/src/main/java/net/pokeranalytics/android/RealmWriteService.kt
  2. 1252
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  3. 5
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  4. 16
      app/src/main/java/net/pokeranalytics/android/calculus/ReportWhistleBlower.kt
  5. 2
      app/src/main/java/net/pokeranalytics/android/model/extensions/SessionExtensions.kt
  6. 7
      app/src/main/java/net/pokeranalytics/android/model/filter/Filterable.kt
  7. 13
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  8. 61
      app/src/main/java/net/pokeranalytics/android/model/realm/FlatTimeInterval.kt
  9. 29
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  10. 257
      app/src/main/java/net/pokeranalytics/android/model/utils/SessionSetManager.kt
  11. 3
      app/src/main/java/net/pokeranalytics/android/ui/fragment/StatisticsFragment.kt
  12. 1
      app/src/main/java/net/pokeranalytics/android/ui/fragment/report/ComposableTableReportFragment.kt
  13. 8
      app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt
  14. 27
      app/src/main/java/net/pokeranalytics/android/ui/modules/feed/FeedFragment.kt
  15. 26
      app/src/main/java/net/pokeranalytics/android/util/extensions/DateExtension.kt

@ -43,9 +43,9 @@ class RealmWriteService : Service() {
this.realm.executeTransactionAsync({ asyncRealm ->
handler(asyncRealm)
Timber.d(">> handler done")
// Timber.d(">> handler done")
}, {
Timber.d(">> YEAAAAAAAAAAAH !!!")
// Timber.d(">> YEAAAAAAAAAAAH !!!")
this.realm.refresh()
}, {
Timber.d(">> NOOOOO error = $it")

@ -64,6 +64,11 @@ class ComputableGroup(val query: Query, var displayedStats: List<Stat>? = null)
return computables
}
fun timeIntervals(realm: Realm): RealmResults<FlatTimeInterval> {
return Filter.queryOn(realm, this.query)
}
/**
* The list of sets to compute
*/

@ -55,18 +55,18 @@ class ReportWhistleBlower(var context: Context) {
val realm = Realm.getDefaultInstance()
this.sessions = realm.where(Session::class.java).findAll()
this.sessions?.addChangeListener { _ ->
sessions = realm.where(Session::class.java).findAll()
sessions?.addChangeListener { _ ->
requestReportLaunch()
}
this.results = realm.where(Result::class.java).findAll()
this.results?.addChangeListener { _ ->
results = realm.where(Result::class.java).findAll()
results?.addChangeListener { _ ->
requestReportLaunch()
}
this.sessionSets = realm.where(SessionSet::class.java).findAll()
this.sessionSets?.addChangeListener { _ ->
sessionSets = realm.where(SessionSet::class.java).findAll()
sessionSets?.addChangeListener { _ ->
requestReportLaunch()
}
@ -156,13 +156,13 @@ class ReportTask(private var whistleBlower: ReportWhistleBlower, var context: Co
}
fun cancel() {
Timber.d("Reportwhistleblower task CANCEL")
// Timber.d("Reportwhistleblower task CANCEL")
this.cancelled = true
}
private fun launchReports() {
Timber.d("====== Report whistleblower launch batch...")
// Timber.d("====== Report whistleblower launch batch...")
CoroutineScope(Dispatchers.Default).launch {

@ -133,7 +133,7 @@ val AbstractList<Session>.hourlyDuration: Double
return intervals.sumOf { it.hourlyDuration }
}
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long) {
class TimeInterval(var start: Date, var end: Date, var breakDuration: Long = 0L) {
val hourlyDuration: Double
get() {

@ -32,9 +32,9 @@ import net.pokeranalytics.android.util.CrashLogging
*
*/
class UnmanagedFilterField(message: String) : Exception(message) {
}
//class UnmanagedFilterField(message: String) : Exception(message) {
//
//}
/**
* Companion-level Interface to indicate an RealmObject class can be filtered and to provide all the fieldNames (eg: parameter's path) needed to be query on.
@ -64,6 +64,7 @@ class FilterHelper {
SessionSet::class.java -> SessionSet.fieldNameForQueryType(queryCondition)
Transaction::class.java -> Transaction.fieldNameForQueryType(queryCondition)
Result::class.java -> Result.fieldNameForQueryType(queryCondition)
FlatTimeInterval::class.java -> FlatTimeInterval.fieldNameForQueryType(queryCondition)
else -> {
CrashLogging.logException(PAIllegalStateException("Filterable type fields are not defined for condition ${queryCondition::class}, class ${T::class}"))
null

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.RealmMigration
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.realm.FlatTimeInterval
import timber.log.Timber
import java.util.*
@ -341,6 +342,18 @@ class PokerAnalyticsMigration : RealmMigration {
crs.addField("id", String::class.java).setRequired("id", true)
crs.addPrimaryKey("id")
}
schema.create("FlatTimeInterval")?.let { fs ->
fs.addField("id", String::class.java).setRequired("id", true)
fs.addPrimaryKey("id")
fs.addField("startDate", Date::class.java).setRequired("startDate", true)
fs.addField("endDate", Date::class.java).setRequired("endDate", true)
fs.addField("duration", Long::class.java)
schema.get("Session")?.let { ss ->
ss.addRealmSetField("flatTimeIntervals", fs)
}
}
currentVersion++
}

@ -0,0 +1,61 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
import io.realm.annotations.RealmClass
import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import java.util.*
@RealmClass
open class FlatTimeInterval : RealmObject(), Filterable {
@PrimaryKey
var id = UUID.randomUUID().toString()
/**
* The start date of the session
*/
var startDate: Date = Date()
set(value) {
field = value
this.computeDuration()
}
/**
* The start date of the session
*/
var endDate: Date = Date()
set(value) {
field = value
this.computeDuration()
}
/**
* the net duration of the session, automatically calculated
*/
var duration: Long = 0L
@LinkingObjects("flatTimeIntervals")
val sessions: RealmResults<Session>? = null
private fun computeDuration() {
duration = endDate.time - startDate.time
}
companion object {
fun fieldNameForQueryType(queryCondition: Class <out QueryCondition>): String? {
Session.fieldNameForQueryType(queryCondition)?.let {
return "sessions.$it"
}
return null
}
}
}

@ -33,10 +33,7 @@ import net.pokeranalytics.android.ui.graph.Graph
import net.pokeranalytics.android.ui.view.*
import net.pokeranalytics.android.ui.view.rows.SessionPropertiesRow
import net.pokeranalytics.android.util.*
import net.pokeranalytics.android.util.extensions.hourMinute
import net.pokeranalytics.android.util.extensions.shortDateTime
import net.pokeranalytics.android.util.extensions.toCurrency
import net.pokeranalytics.android.util.extensions.toMinutes
import net.pokeranalytics.android.util.extensions.*
import java.text.DateFormat
import java.text.NumberFormat
import java.text.ParseException
@ -202,6 +199,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
*/
var startDate: Date? = null
set(value) {
val previous = this.startDate
field = value
if (value == null) {
startDateHourMinuteComponent = null
@ -217,7 +215,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
if (value != null && this.endDate != null && value.after(this.endDate)) {
this.endDate = null
}
this.dateChanged()
SessionSetManager.startChanged(this, min(previous, value))
// this.computeStats()
}
@ -227,6 +227,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
@Index
var endDate: Date? = null
set(value) {
val previous = this.endDate
field = value
if (value == null) {
endDateHourMinuteComponent = null
@ -237,7 +238,7 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
}
this.computeNetDuration()
this.dateChanged()
SessionSetManager.endChanged(this, max(previous, value))
this.defineDefaultTournamentBuyinIfNecessary()
// this.computeStats()
}
@ -373,6 +374,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
// The custom fields values
var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()
// The list of opponents who participated to the session
var flatTimeIntervals: RealmList<FlatTimeInterval> = RealmList()
// The number of hands played during the sessions
var handsCount: Int? = null
@ -380,10 +384,6 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.generateStakes()
}
private fun dateChanged() {
SessionSetManager.sessionDateChanged(this)
}
// /**
// * Manages impacts on SessionSets
// * Should be called when the start / end date are changed
@ -697,10 +697,8 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
CrashLogging.log("Deletes session. Id = ${this.id}")
if (isValid) {
// realm.executeTransaction {
cleanup()
deleteFromRealm()
// }
cleanup()
deleteFromRealm()
} else {
CrashLogging.log("Attempt to delete an invalid session")
}
@ -715,6 +713,9 @@ open class Session : RealmObject(), Savable, RowUpdatable, RowRepresentable, Tim
this.sessionSet?.let {
SessionSetManager.removeFromTimeline(this)
}
SessionSetManager.sessionDateChanged(this)
// cleanup unnecessary related objects
this.result?.deleteFromRealm()
this.computableResult?.deleteFromRealm()

@ -1,13 +1,18 @@
package net.pokeranalytics.android.model.utils
import io.realm.Realm
import io.realm.RealmModel
import io.realm.RealmQuery
import io.realm.RealmResults
import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.realm.FlatTimeInterval
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.SessionSet
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.util.extensions.max
import net.pokeranalytics.android.util.extensions.min
import timber.log.Timber
import java.util.*
class CorruptSessionSetException(message: String) : Exception(message)
@ -17,12 +22,25 @@ class CorruptSessionSetException(message: String) : Exception(message)
*/
object SessionSetManager {
var sessions: RealmResults<Session>
var sessions: RealmResults<Session>? = null
private val sessionIdsToProcess = mutableSetOf<String>()
private var start: Date? = null
private var end: Date? = null
fun configure() {} // launch init
fun startChanged(session: Session, date: Date?) {
this.start = min(this.start, date)
this.sessionIdsToProcess.add(session.id)
}
fun endChanged(session: Session, date: Date?) {
this.end = max(this.end, date)
this.sessionIdsToProcess.add(session.id)
}
fun sessionDateChanged(session: Session) {
this.sessionIdsToProcess.add(session.id)
}
@ -31,11 +49,12 @@ object SessionSetManager {
val realm = Realm.getDefaultInstance()
this.sessions = realm.where(Session::class.java).findAllAsync()
this.sessions.addChangeListener { _, _ ->
if (this.sessionIdsToProcess.isNotEmpty()) {
sessions = realm.where(Session::class.java).findAllAsync()
sessions?.addChangeListener { _, _ ->
if (this.start != null && this.end != null) {
realm.executeTransactionAsync { asyncRealm ->
processSessions(asyncRealm)
cleanUp()
}
}
}
@ -43,21 +62,38 @@ object SessionSetManager {
realm.close()
}
private fun cleanUp() {
this.start = null
this.end = null
// this.sessionIdsToProcess.clear()
}
private fun processSessions(realm: Realm) {
Timber.d("***** processSessions, process count = ${sessionIdsToProcess.size}")
// Timber.d("***** processSessions, process count = ${sessionIdsToProcess.size}")
for (sessionId in this.sessionIdsToProcess) {
realm.findById<Session>(sessionId)?.let { session ->
if (session.startDate != null && session.endDate != null) {
updateTimeline(session)
} else if (session.sessionSet != null) {
removeFromTimeline(session)
}
val start = this.start
val end = this.end
val sessions = sessionIdsToProcess.mapNotNull { realm.findById<Session>(it) }
for (session in sessions) {
// Session Sets
val startDate = session.startDate
val endDate = session.endDate
if (startDate != null && endDate != null) {
updateTimeline(session)
} else if (session.sessionSet != null) {
removeFromTimeline(session)
}
}
// FlatTimeIntervals
if (start != null && end != null) {
processFlatTimeInterval(realm, start, end)
}
this.sessionIdsToProcess.clear()
}
/**
@ -76,25 +112,37 @@ object SessionSetManager {
throw ModelException("End date should never be null here")
}
val sessionSets = this.matchingSets(session)
cleanupSessionSets(session, sessionSets)
val start = session.startDate!!
val end = session.endDate!!
// val sessionId = session.id
// realm.executeTransactionAsync { asyncRealm ->
// asyncRealm.findById<Session>(sessionId)?.let { s ->
// val sessionSets = this.matchingSets(session)
// cleanupSessionSets(session, sessionSets)
// }
// }
val sessionSets = this.matchingData<SessionSet>(session.realm, start, end)
cleanupSessionSets(session, sessionSets)
}
private fun matchingSets(session: Session): RealmResults<SessionSet> {
val realm = session.realm
val endDate = session.endDate!! // tested above
val startDate = session.startDate!!
val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
// private fun matchingSets(session: Session): RealmResults<SessionSet> {
// val realm = session.realm
// val endDate = session.endDate!! // tested above
// val startDate = session.startDate!!
//
// val query: RealmQuery<SessionSet> = realm.where(SessionSet::class.java)
//
// query
// .lessThanOrEqualTo("startDate", startDate)
// .greaterThanOrEqualTo("endDate", startDate)
// .or()
// .lessThanOrEqualTo("startDate", endDate)
// .greaterThanOrEqualTo("endDate", endDate)
// .or()
// .greaterThanOrEqualTo("startDate", startDate)
// .lessThanOrEqualTo("endDate", endDate)
//
// return query.findAll()
// }
private inline fun <reified T : RealmModel> matchingData(realm: Realm, startDate: Date, endDate: Date): RealmResults<T> {
val query: RealmQuery<T> = realm.where(T::class.java)
query
.lessThanOrEqualTo("startDate", startDate)
@ -127,7 +175,7 @@ object SessionSetManager {
sessionSets.deleteAllFromRealm()
allImpactedSessions.forEach { impactedSession ->
val sets = matchingSets(impactedSession)
val sets = matchingData<SessionSet>(impactedSession.realm, impactedSession.startDate!!, impactedSession.endDate!!)
this.updateTimeFrames(sets, impactedSession)
}
@ -249,4 +297,155 @@ object SessionSetManager {
}
}
private fun processFlatTimeInterval(realm: Realm, start: Date, end: Date) {
val sessions = matchingData<Session>(realm, start, end)
val intervalsStore = IntervalsStore(sessions.toSet())
intervalsStore.intervals.forEach { it.deleteFromRealm() }
val intervals = SessionInterval.intervalMap(intervalsStore.sessions)
for (interval in intervals) {
val sortedDates = interval.dates.sorted()
for (i in (0 until sortedDates.size - 1)) {
val s = sortedDates[i]
val e = sortedDates[i + 1]
val matchingSessions = interval.sessions.filter {
val sd = it.startDate
val ed = it.endDate
(sd != null && ed != null && sd <= s && ed >= e)
}
if (matchingSessions.isNotEmpty()) {
Timber.d("**** Create FTI: $s - $e")
val fti = FlatTimeInterval()
fti.startDate = s
fti.endDate = e
matchingSessions.forEach { it.flatTimeIntervals.add(fti) }
realm.insertOrUpdate(fti)
} else {
Timber.w("The FTI has no sessions")
}
}
}
}
}
class IntervalsStore(sessions: Set<Session>) {
var start: Date = Date()
var end: Date = Date(0L)
val intervals = mutableSetOf<FlatTimeInterval>()
val sessions = mutableSetOf<Session>()
private val sessionIds: MutableSet<String> = mutableSetOf()
init {
processSessions(sessions)
}
private fun processSessions(sessions: Set<Session>) {
this.sessions.addAll(sessions)
for (session in sessions) {
loadIntervals(session)
}
}
private fun loadIntervals(session: Session) {
if (sessionIds.contains(session.id)) {
return
}
session.startDate?.let { this.start = min(this.start, it) }
session.endDate?.let { this.end = max(this.end, it) }
this.sessionIds.add(session.id)
for (fti in session.flatTimeIntervals) {
this.intervals.add(fti)
fti.sessions?.let { sessions ->
for (s in sessions) {
loadIntervals(s)
}
}
}
}
}
class SessionInterval(session: Session) {
var start: Date
var end: Date?
var sessions: MutableSet<Session> = mutableSetOf()
val dates: MutableSet<Date> = mutableSetOf()
val duration: Long
get() {
val endDate = end ?: Date()
return endDate.time - start.time
}
init {
this.start = session.startDate!!
this.end = session.endDate
this.addSession(session)
}
private fun addSession(session: Session) {
this.sessions.add(session)
session.startDate?.let { this.dates.add(it) }
session.endDate?.let { endDate ->
this.dates.add(endDate)
if (endDate > end) {
end = endDate
}
}
}
companion object {
fun intervalMap(sessions: Set<Session>): List<SessionInterval> {
val sorted = sessions.sortedBy { it.startDate }
val intervals = mutableListOf<SessionInterval>()
sorted.firstOrNull()?.let { firstSession ->
var currentInterval = SessionInterval(firstSession)
intervals.add(currentInterval)
for (session in sessions.drop(1)) {
val start = session.startDate!!
val currentEnd = currentInterval.end
if (currentEnd != null && start > currentEnd) {
val interval = SessionInterval(session)
currentInterval = interval
intervals.add(interval)
} else {
currentInterval.addSession(session)
}
}
}
intervals.forEach {
Timber.d("s = ${it.start}, e = ${it.end}")
}
return intervals
}
}
}

@ -167,7 +167,7 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
val async = async {
val s = Date()
Timber.d(">>> start...")
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()
@ -199,7 +199,6 @@ class StatisticsFragment : FilterableFragment(), RealmAsyncListener {
Timber.d(">>> Launch statistics computations")
val filter: Filter? = this.currentFilter(this.requireContext(), realm)?.let {
if (it.filterableType == currentFilterable) { it } else { null }
}

@ -27,6 +27,7 @@ import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rows.StatRow
import net.pokeranalytics.android.util.NULL_TEXT
import net.pokeranalytics.android.util.TextFormat
import timber.log.Timber
open class ComposableTableReportFragment : RealmFragment(), StaticRowRepresentableDataSource,
RowRepresentableDelegate {

@ -388,7 +388,7 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
val async = async {
val s = Date()
Timber.d(">>> start...")
// Timber.d(">>> start...")
val realm = Realm.getDefaultInstance()
realm.refresh()
@ -436,6 +436,8 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
private fun launchStatComputation(realm: Realm) {
return
Timber.d(">>> Launch calendar computations")
val calendar = Calendar.getInstance()
@ -660,8 +662,8 @@ class CalendarFragment : RealmFragment(), StaticRowRepresentableDataSource,
}
}
Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
Timber.d("Rows: ${rows.size}")
// Timber.d("Display data: ${System.currentTimeMillis() - startDate.time}ms")
// Timber.d("Rows: ${rows.size}")
this.calendarAdapter.notifyDataSetChanged()

@ -19,9 +19,7 @@ import net.pokeranalytics.android.api.BlogPostApi
import net.pokeranalytics.android.databinding.FragmentFeedBinding
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.model.realm.Transaction
import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.model.realm.handhistory.HandHistory
import net.pokeranalytics.android.ui.activity.BillingActivity
import net.pokeranalytics.android.ui.activity.components.RequestCode
@ -87,10 +85,10 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
override fun asyncListenedEntityChange(realm: Realm, clazz: Class<out RealmModel>) {
Timber.d("asyncListenedEntityChange for $clazz")
// Timber.d("asyncListenedEntityChange for $clazz")
when (clazz.kotlin) {
Session::class -> {
Timber.d("WOWOWOOWOOWOWOWOWOWOWOWOWO")
// Timber.d("WOWOWOOWOOWOWOWOWOWOWOWOWO")
this.sessionAdapter.refreshData()
this.sessionAdapter.notifyDataSetChanged()
}
@ -286,9 +284,20 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
displayBlogPostButton()
binding.postButton.setOnClickListener {
Preferences.setBlogTipsTapped(requireContext())
parentActivity?.openUrl(URL.BLOG_TIPS.value)
displayBlogPostButton()
getRealm().executeTransactionAsync { realm ->
realm.where<Session>().findAll().deleteAllFromRealm()
realm.where<SessionSet>().findAll().deleteAllFromRealm()
realm.where<FlatTimeInterval>().findAll().deleteAllFromRealm()
realm.where<Result>().findAll().deleteAllFromRealm()
realm.where<ComputableResult>().findAll().deleteAllFromRealm()
}
// Preferences.setBlogTipsTapped(requireContext())
// parentActivity?.openUrl(URL.BLOG_TIPS.value)
// displayBlogPostButton()
}
binding.postButton.viewTreeObserver.addOnGlobalLayoutListener {
@ -632,7 +641,7 @@ class FeedFragment : FilterableFragment(), RowRepresentableDelegate, PurchaseLis
show = true
this.badgeDrawable?.number = newCount
}
this.binding.postButton.isVisible = show
this.binding.postButton.isVisible = true
this.badgeDrawable?.isVisible = show
}

@ -7,6 +7,32 @@ import java.util.*
// Calendar
fun min(d1: Date, d2: Date): Date {
return if (d1 < d2) d1 else d2
}
fun max(d1: Date, d2: Date): Date {
return if (d1 > d2) d1 else d2
}
@JvmName("min1")
fun min(d1: Date?, d2: Date?): Date? {
return if (d1 != null) {
if (d2 != null) min(d1, d2) else d1
} else {
d2
}
}
@JvmName("max1")
fun max(d1: Date?, d2: Date?): Date? {
return if (d1 != null) {
if (d2 != null) max(d1, d2) else d1
} else {
d2
}
}
// Return a double representing the hour / minute of a date from a calendar
fun Calendar.hourMinute(): Double {
return (this.get(Calendar.HOUR_OF_DAY) + this.get(Calendar.MINUTE).toDouble() / 60.0).roundOffDecimal()

Loading…
Cancel
Save