Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Examples/OneSignalDemo/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ android {
// signingConfig null
// productFlavors.huawei.signingConfig signingConfigs.huawei
debuggable true
// Note: profileable is automatically enabled when debuggable=true
// Enable method tracing for detailed performance analysis
testCoverageEnabled false
}
// Profileable release build for performance testing
profileable {
initWith release
debuggable false
profileable true
minifyEnabled false
signingConfig signingConfigs.debug
matchingFallbacks = ['release']
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package com.onesignal.sdktest.application
* - Cleaner code structure
* - Proper ANR prevention
*
* @see MainApplication (deprecated Java version)
* @see MainApplication.java (deprecated Java version)
*/
import android.annotation.SuppressLint
import android.os.StrictMode
Expand Down Expand Up @@ -39,10 +39,15 @@ import com.onesignal.user.state.IUserStateObserver
import com.onesignal.user.state.UserChangedState
import com.onesignal.user.state.UserState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class MainApplicationKT : MultiDexApplication() {

private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

init {
// run strict mode to surface any potential issues easier
StrictMode.enableDefaults()
Expand All @@ -64,20 +69,23 @@ class MainApplicationKT : MultiDexApplication() {
OneSignalNotificationSender.setAppId(appId)

// Initialize OneSignal asynchronously on background thread to avoid ANR
CoroutineScope(Dispatchers.IO).launch {
val success = OneSignal.initWithContextSuspend(this@MainApplicationKT, appId)
Log.d(Tag.LOG_TAG, "OneSignal async init success: $success")

if (success) {
applicationScope.launch {
try {
OneSignal.initWithContextSuspend(this@MainApplicationKT, appId)
Log.d(Tag.LOG_TAG, "OneSignal async init completed")

// Set up all OneSignal listeners after successful async initialization
setupOneSignalListeners()

// Request permission - this will internally switch to Main thread for UI operations
OneSignal.Notifications.requestPermission(true)

Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT)

} catch (e: Exception) {
Log.e(Tag.LOG_TAG, "OneSignal initialization error", e)
}
Comment on lines +85 to 87
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should try-catch here:

  1. Not something our customers need to do
  2. In development I would rather the app crash so we know there is a major issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok i will merge this into the refactor branch and fix it there. thanks

}

Log.d(Tag.LOG_TAG, Text.ONESIGNAL_SDK_INIT)
}

private fun setupOneSignalListeners() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.onesignal.common.threading

import com.onesignal.common.AndroidUtils
import com.onesignal.common.threading.OneSignalDispatchers.BASE_THREAD_NAME
import com.onesignal.debug.internal.logging.Logging
import kotlinx.coroutines.CompletableDeferred
import java.util.concurrent.CountDownLatch
Expand Down Expand Up @@ -101,12 +102,31 @@ class CompletionAwaiter(
}

private fun logAllThreads(): String {
val allThreads = Thread.getAllStackTraces()
val sb = StringBuilder()
for ((thread, stack) in allThreads) {
sb.append("ThreadDump Thread: ${thread.name} [${thread.state}]\n")
for (element in stack) {
sb.append("\tat $element\n")

// Add OneSignal dispatcher status first (fast)
sb.append("=== OneSignal Dispatchers Status ===\n")
sb.append(OneSignalDispatchers.getStatus())
sb.append("=== OneSignal Dispatchers Performance ===\n")
sb.append(OneSignalDispatchers.getPerformanceMetrics())
sb.append("\n\n")

// Add lightweight thread info (fast)
sb.append("=== All Threads Summary ===\n")
val threads = Thread.getAllStackTraces().keys
for (thread in threads) {
sb.append("Thread: ${thread.name} [${thread.state}] ${if (thread.isDaemon) "(daemon)" else ""}\n")
}

// Only add full stack traces for OneSignal threads (much faster)
Comment thread
jkasten2 marked this conversation as resolved.
sb.append("\n=== OneSignal Thread Details ===\n")
for ((thread, stack) in Thread.getAllStackTraces()) {
if (thread.name.startsWith(BASE_THREAD_NAME)) {
sb.append("Thread: ${thread.name} [${thread.state}]\n")
for (element in stack.take(10)) { // Limit to first 10 frames
sb.append("\tat $element\n")
}
sb.append("\n")
}
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package com.onesignal.common.threading

import com.onesignal.debug.internal.logging.Logging
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger

/**
* Optimized threading manager for the OneSignal SDK.
*
* Performance optimizations:
* - Lazy initialization to reduce startup overhead
* - Custom thread pools for both IO and Default operations
* - Optimized thread pool configuration (smaller pools)
* - Small bounded queues (10 tasks) to prevent memory bloat
* - Reduced context switching overhead
* - Efficient thread management with controlled resource usage
*/
internal object OneSignalDispatchers {
// Optimized pool sizes based on CPU cores and workload analysis
private const val IO_CORE_POOL_SIZE = 2 // Increased for better concurrency
private const val IO_MAX_POOL_SIZE = 3 // Increased for better concurrency
private const val DEFAULT_CORE_POOL_SIZE = 2 // Optimal for CPU operations
private const val DEFAULT_MAX_POOL_SIZE = 3 // Slightly larger for CPU operations
private const val KEEP_ALIVE_TIME_SECONDS =
30L // Keep threads alive longer to reduce recreation
private const val QUEUE_CAPACITY =
10 // Small queue that allows up to 10 tasks to wait in queue when all threads are busy
internal const val BASE_THREAD_NAME = "OneSignal" // Base thread name prefix
private const val IO_THREAD_NAME_PREFIX =
"$BASE_THREAD_NAME-IO" // Thread name prefix for I/O operations
private const val DEFAULT_THREAD_NAME_PREFIX =
"$BASE_THREAD_NAME-Default" // Thread name prefix for CPU operations

private class OptimizedThreadFactory(
private val namePrefix: String,
private val priority: Int = Thread.NORM_PRIORITY,
) : ThreadFactory {
private val threadNumber = AtomicInteger(1)

override fun newThread(r: Runnable): Thread {
val thread = Thread(r, "$namePrefix-${threadNumber.getAndIncrement()}")
thread.isDaemon = true
thread.priority = priority
return thread
}
}

private val ioExecutor: ThreadPoolExecutor by lazy {
try {
ThreadPoolExecutor(
IO_CORE_POOL_SIZE,
IO_MAX_POOL_SIZE,
KEEP_ALIVE_TIME_SECONDS,
TimeUnit.SECONDS,
LinkedBlockingQueue(QUEUE_CAPACITY),
OptimizedThreadFactory(
namePrefix = IO_THREAD_NAME_PREFIX,
priority = Thread.NORM_PRIORITY - 1,
// Slightly lower priority for I/O tasks
),
).apply {
allowCoreThreadTimeOut(false) // Keep core threads alive
}
} catch (e: Exception) {
Logging.error("OneSignalDispatchers: Failed to create IO executor: ${e.message}")
throw e // Let the dispatcher fallback handle this
}
}

private val defaultExecutor: ThreadPoolExecutor by lazy {
try {
ThreadPoolExecutor(
DEFAULT_CORE_POOL_SIZE,
DEFAULT_MAX_POOL_SIZE,
KEEP_ALIVE_TIME_SECONDS,
TimeUnit.SECONDS,
LinkedBlockingQueue(QUEUE_CAPACITY),
OptimizedThreadFactory(DEFAULT_THREAD_NAME_PREFIX),
).apply {
allowCoreThreadTimeOut(false) // Keep core threads alive
}
} catch (e: Exception) {
Logging.error("OneSignalDispatchers: Failed to create Default executor: ${e.message}")
throw e // Let the dispatcher fallback handle this
}
}

// Dispatchers and scopes - also lazy initialized
val IO: CoroutineDispatcher by lazy {
try {
ioExecutor.asCoroutineDispatcher()
} catch (e: Exception) {
Logging.error("OneSignalDispatchers: Using fallback Dispatchers.IO dispatcher: ${e.message}")
Dispatchers.IO
}
}

val Default: CoroutineDispatcher by lazy {
try {
defaultExecutor.asCoroutineDispatcher()
} catch (e: Exception) {
Logging.error("OneSignalDispatchers: Using fallback Dispatchers.Default dispatcher: ${e.message}")
Dispatchers.Default
}
}

private val IOScope: CoroutineScope by lazy {
CoroutineScope(SupervisorJob() + IO)
}

private val DefaultScope: CoroutineScope by lazy {
CoroutineScope(SupervisorJob() + Default)
}
Comment thread
jkasten2 marked this conversation as resolved.

fun launchOnIO(block: suspend () -> Unit) {
IOScope.launch { block() }
}

fun launchOnDefault(block: suspend () -> Unit) {
DefaultScope.launch { block() }
}

internal fun getPerformanceMetrics(): String {
return try {
"""
OneSignalDispatchers Performance Metrics:
- IO Pool: ${ioExecutor.activeCount}/${ioExecutor.corePoolSize} active/core threads
- IO Queue: ${ioExecutor.queue.size} pending tasks
- Default Pool: ${defaultExecutor.activeCount}/${defaultExecutor.corePoolSize} active/core threads
- Default Queue: ${defaultExecutor.queue.size} pending tasks
- Total completed tasks: ${ioExecutor.completedTaskCount + defaultExecutor.completedTaskCount}
- Memory usage: ~${(ioExecutor.activeCount + defaultExecutor.activeCount) * 1024}KB (thread stacks, ~1MB each)
""".trimIndent()
} catch (e: Exception) {
"OneSignalDispatchers not initialized or using fallback dispatchers ${e.message}"
}
}

internal fun getStatus(): String {
val ioExecutorStatus =
try {
if (ioExecutor.isShutdown) "Shutdown" else "Active"
} catch (e: Exception) {
"ioExecutor Not initialized ${e.message ?: "Unknown error"}"
}

val defaultExecutorStatus =
try {
if (defaultExecutor.isShutdown) "Shutdown" else "Active"
} catch (e: Exception) {
"defaultExecutor Not initialized ${e.message ?: "Unknown error"}"
}

val ioScopeStatus =
try {
if (IOScope.isActive) "Active" else "Cancelled"
} catch (e: Exception) {
"IOScope Not initialized ${e.message ?: "Unknown error"}"
}

val defaultScopeStatus =
try {
if (DefaultScope.isActive) "Active" else "Cancelled"
} catch (e: Exception) {
"DefaultScope Not initialized ${e.message ?: "Unknown error"}"
}

return """
OneSignalDispatchers Status:
- IO Executor: $ioExecutorStatus
- Default Executor: $defaultExecutorStatus
- IO Scope: $ioScopeStatus
- Default Scope: $defaultScopeStatus
""".trimIndent()
}
}
Loading
Loading