0

I'm developing an application that monitors customer's sales calls and uploads them to the server. I'm using following periodic method in a service to auto-upload calls that were not synced. It runs after every 0.5Hr delay.

After reducing the delay to 5s I noticed that the memory heap keeps on increasing every 5s. I quickly figured out that setting up callLogInfo is causing the memory issue which on long run leads to lags, memory leaks, slow phones.

GlobalScope.launch(Dispatchers.IO) {
    val projection = arrayOf(
        "_id",
        CallLog.Calls.NUMBER,
        CallLog.Calls.DATE,
        CallLog.Calls.DURATION,
        CallLog.Calls.TYPE,
        CallLog.Calls.CACHED_NAME,
        CallLog.Calls.PHONE_ACCOUNT_ID
    )
    val contentResolver = App.getContext().applicationContext.contentResolver
    val helper: DatabaseHelper = DatabaseHelper.getInstance(App.getContext())
    val callLogInfo = CallLogInfo()

    while (true) {
        Log.d("CallMonitorService", "Scanner loop")
        try {
            contentResolver.query(
                CallLog.Calls.CONTENT_URI,
                projection,
                null,
                null,
                CallLog.Calls.DEFAULT_SORT_ORDER
            )?.also { cursor ->
                cursor.moveToFirst()

                while (!cursor.isAfterLast) {
                    // Following 6 lines were the main cause of increasing memory heap
                    callLogInfo.name = cursor.getString(cursor.getColumnIndex(CallLog.Calls.CACHED_NAME))
                    callLogInfo.number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER))
                    callLogInfo.callType = cursor.getString(cursor.getColumnIndex(CallLog.Calls.TYPE))
                    callLogInfo.date = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DATE))
                    callLogInfo.duration = cursor.getLong(cursor.getColumnIndex(CallLog.Calls.DURATION))
                    callLogInfo.simId = cursor.getStringOrNull(cursor.getColumnIndex(CallLog.Calls.PHONE_ACCOUNT_ID))

                    // If Call is not marked as "SYNCED" in Database, post the call for upload on a different handler
                    // It also marks the call as "SYNCED" after successful upload
                    // This is rarely called because most of the calls are synced from a BroadcastReceiver
                    if (helper.getStatus(callLogInfo.getUID()) != "SYNCED") {
                        UploadUtil().uploadCallLogAsync(App.getContext(), callLogInfo)
                    }
                    cursor.moveToNext()
                }
                cursor.close()
            }
        } catch (e: Exception) {
            Log.d("CallMonitorService","Exception in scanner loop: $e")
        }
        delay(5000) // (In ms) Actually it is 1800 * 1000 but for testing I use 5 * 1000
    }
}
  • callLogInfo class
class CallLogInfo {
    var name: String? = null
    var number: String? = null
    var callType: String? = null
    var date: Long = 0
    var duration: Long = 0
    var simId: String? = null

    fun getUID(): String {
        return "${this.number}_${this.callType}_${this.date}"
    }
}
  • Increasing memory heap

Increasing memory heap

I know that for periodic tasks it is recommended to use either Handler + postDelayed or ScheduledExecutorService. I tried both the methods but faced the same issue so decided to post the original code.

Can anyone suggest a better and efficient approach to this or suggest some refactoring?

Thank you!!

PROxZIMA
  • 31
  • 1
  • 4
  • Maybe the query code has memory leak. Or "UploadUtil().uploadCallLogAsync" should be blamed for the memory leak. You can delete "UploadUtil().uploadCallLogAsync" and test again. – simon5678 Feb 12 '22 at 11:00
  • @simon5678 "uploadCallLogAsync" is not the culprit. I tested by commenting that part. Also it has its own handler to work on – PROxZIMA Feb 12 '22 at 13:10
  • What's the cursor's size? Print cursor.getCount(). I guess it is quite large, maybe 100? – simon5678 Feb 12 '22 at 13:54
  • `cursor` is representing all the call logs. For me the count is around 1500. For corporate phones the count around 3k-4K. – PROxZIMA Feb 12 '22 at 16:36
  • 1
    In my experience, Java's Garbage Collection happens automatically during the lifetime of a program. It means that it may run now or it may run in next minute. The cursor is too large, and it will create many temporary String objects. So the heap will keep on increasing. – simon5678 Feb 12 '22 at 16:56
  • Yes, I noticed it while recording heap dump. Can you suggest any proper method to run a memory consuming periodic task? – PROxZIMA Feb 12 '22 at 17:41

1 Answers1

0

It seems that you call UploadUtil().uploadCallLogAsync for every single call log. Why don't upload them once in bulk? This will reduce a lot of data cost and battery cost.

Then you may use registerContentObserver(CallLog.Calls.CONTENT_URI) to watch the change of call logs, instead of uploading all the call logs everytime. (https://stackoverflow.com/a/4435473/2173056)

simon5678
  • 249
  • 1
  • 5
  • Nice solution but what if the user force stop the app, did few calls and start the app again. Will `registerContentObserver` notify about those few calls which the app missed? – PROxZIMA Feb 13 '22 at 04:55
  • ```registerContentObserver``` only watch for changes after you register it. You can check the cursor count and the last call log, so that you can know if there are new call logs since the app is closed last time. – simon5678 Feb 13 '22 at 07:46