Skip to content

Conversation

gmaclennan
Copy link
Contributor

Summary

⚠️ Android-only for now ⚠️

This PR moves this module to use the Expo Modules API with support for the React Native New Architecture with performance optimizations for bridge messaging resulting in 2.3x to 4.1x faster message throughput.

🚀 Performance Results

Message Processing Benchmark

Sending/receiving 1,000 JSON messages, randomly generated:

{
  avatar: "https://cdn.jsdelivr.net/gh/faker-js/assets-person-portrait/female/512/42.jpg",
  birthday: "1979-11-26T14:05:09.564Z",
  email: "Tobin.Kertzmann@gmail.com",
  firstName: "Lillie",
  id: 1,
  lastName: "Waelchi",
  sex: "female",
  subscriptionTier: "free",
  uuid: "b9b423e7-01ec-43e3-8187-382c7af298e6",
}
Device Architecture Before After Improvement
Moto G4 Legacy RN 1,252ms 536ms 2.3x faster
Moto G4 New RN 1,252ms 409ms 3.1x faster
Galaxy A14 Legacy RN 1,129ms 354ms 3.2x faster
Galaxy A14 New RN 1,129ms 273ms 4.1x faster

Per-Message Performance

Metric Before After (Legacy) After (New RN) Improvement
Avg per-message (Moto G4) 1.25ms 0.54ms 0.41ms 2.3-3.1x faster
Avg per-message (Galaxy A14) 1.13ms 0.35ms 0.27ms 3.2-4.1x faster

🔧 Key Optimizations

1. Message Batching in libuv Callbacks

  • Before: Processed messages one-by-one with individual lock cycles
  • After: Process up to 8 messages per callback, minimizing lock contention
  • Impact: Reduces libuv callback overhead and thread synchronization costs
// Extract batch of messages under lock
while (batch_count < MAX_BATCH_SIZE && !this->messageQueue.empty()) {
    messages[batch_count] = this->messageQueue.front();
    this->messageQueue.pop();
    batch_count++;
}
// Process batch outside of lock to minimize lock contention

2. Main Thread Elimination for Message Dispatch

  • Before: Messages routed through main UI thread, causing potential blocking
  • After: Direct dispatch from JNI thread to dedicated Node.js event dispatcher
  • Implementation: Lock-free channel-based messaging with unlimited capacity
// Dedicated thread for Node.js events - ensures order and keeps off main thread
private val nodeEventDispatcher = Executors.newSingleThreadExecutor { r ->
    Thread(r, "NodeJS-Event-Dispatcher").apply {
        priority = Thread.MAX_PRIORITY // High priority for low latency
    }
}.asCoroutineDispatcher()

// Channel for lock-free message passing from Node.js thread to event dispatcher
private val messageChannel = Channel<MessageData>(Channel.UNLIMITED)

Benefits:

  • Zero Main Thread Blocking: UI remains responsive during heavy messaging
  • Lock-Free Architecture: No synchronization overhead between threads
  • Dedicated High-Priority Thread: Ensures consistent low-latency message processing
  • Order Preservation: Single-threaded dispatcher maintains message ordering

3. JNI Performance Improvements

  • Thread-Local Storage: Cache JNIEnv per thread to avoid repeated attachment
  • Global Reference Caching: Cache class and method references
  • Memory Management: Prevent thread attachment exhaustion with safety limits
// Fast path JNI environment getter using thread-local storage
static thread_local JNIEnv* tls_env = nullptr;
static thread_local bool tls_attached = false;

4. Smart Async Notification

  • Before: Multiple uv_async_send() calls for rapid message bursts
  • After: Only notify when queue transitions from empty to non-empty
  • Impact: Prevents redundant libuv wake-ups during high-frequency messaging

5. Async Initialization with Coroutines

  • Before: Blocking file extraction during module initialization
  • After: Concurrent asset extraction using Kotlin coroutines with semaphore-controlled parallelism
private fun asyncInit() {
    moduleScope.launch {
        // Parallel extraction of native assets and builtin modules
        val nativeAssetsJob = async { copyNativeAssetsFrom() }
        val builtinModulesJob = async {
            copyAssetsFrom(
                nodeJsProjectPath, 
                NODEJS_PROJECT_DIR,
                "Copying Node.js project files"
            )
        }
        
        // Wait for both to complete
        awaitAll(nativeAssetsJob, builtinModulesJob)
    }
}

Initialization Improvements:

  • Non-Blocking Startup: App initialization no longer waits for asset extraction
  • Parallel File Operations: Native assets and Node.js modules copied concurrently
  • Semaphore-Controlled I/O: Limits concurrent file operations to prevent resource exhaustion
  • Memory Leak Prevention: Proper coroutine cleanup and resource management

6. Architecture Modernization

  • Migrated from legacy Java to Kotlin with Coroutines
  • Converted to Expo Native Module architecture
  • Updated build system for modern React Native compatibility
  • Added comprehensive memory leak prevention and cleanup

📊 Technical Overview

Message Processing Pipeline Improvements

  1. Eliminated Main Thread Dependency: Messages flow directly from JNI → dedicated thread → React Native
  2. Reduced Lock Retention: Batch extraction minimizes critical section time
  3. Lock-Free Communication: Channel-based messaging between threads
  4. JNI Optimization: Thread-local caching reduces attachment overhead
  5. Memory Safety: Proper cleanup and resource management

Thread Synchronization Benefits

The new implementation improves thread synchronization by:

  • Acquiring locks once per batch instead of once per message
  • Processing messages outside critical sections
  • Using atomic operations for thread counting and safety limits
  • Eliminating main thread involvement in message dispatch

Initialization Performance

  • Concurrent Asset Extraction: Multiple files copied in parallel
  • Resource-Controlled I/O: Semaphore prevents system overload
  • Non-Blocking Startup: App becomes responsive immediately while assets copy in background

🏗️ Breaking Changes

  • Package Structure: Updated from com.janeasystems.* to com.nodejsmobile.reactnative.*
  • Build System: Now requires Expo Module Core dependencies
  • Minimum Requirements: Updated Android build tools and SDK versions

🧪 Testing

Benchmarking performed using a dedicated test app with:

  • 1,000 JSON messages per test run
  • Multiple runs averaged for reliability
  • Testing across legacy and new React Native architectures
  • Real device testing (Moto G4, Galaxy A14)

…nce improvements

Major architectural improvements:
- Convert from thread-based to coroutine-based async operations
- Replace Semaphore with CompletableDeferred for proper initialization signaling
- Add structured concurrency with CoroutineScope and proper cleanup

Performance optimizations:
- Implement parallel file copying with semaphore-limited concurrency (max 8)
- Convert all I/O operations to suspend functions with Dispatchers.IO
- Add parallel processing to asset folder copying and native assets
- Replace manual file operations with efficient Kotlin stdlib methods

API improvements:
- Convert string path parameters to File objects for type safety
- Replace verbose boolean extraction with reusable extension function
- Modernize file operations using buffered streams and proper resource management
- Remove redundant wrapper functions and simplify error handling

Code quality enhancements:
- Replace wildcard imports with specific imports for better clarity
- Convert properties from string paths to File instances
- Eliminate race conditions in initialization with proper async coordination
- Add comprehensive error handling with structured exception propagation
- Simplify recursive file deletion using Kotlin's deleteRecursively()

Threading model improvements:
- Replace Thread creation with coroutine launch for all background operations
- Use proper dispatchers (Main for React Native events, IO for file operations)
- Implement cancellable operations that respect module lifecycle
- Add semaphore-based resource limiting to prevent system overload
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant