From 166347ead93ac1128d7519d5211eb32e53659c50 Mon Sep 17 00:00:00 2001 From: Parsa Nasirimehr Date: Thu, 16 Jan 2025 07:34:03 -0800 Subject: [PATCH] chore(Android): migrate MessageQueueThread and it's implementation to Kotlin (#48652) Summary: Continuing our usual journey, this time migrating MessageQueueThread. Was not expecting to see that many assertions in ReactContext. One important thing to note on this PR: I had to add an extra Throw RuntimeException to `startNewBackgroundThread`. It already had one from the`dataFuture.getOrThrow()`, but if your thread is not associated with a Looper, calling `myLooper` (Line 203 of MessageQueueThreadImpl) Can return null. Until now, this would have been a `NPE`, i just decided to make it a `RuntimeException` with the message `Looper not found for thread`. Let me know if you want me to make that function return a nullable, or throw another message or exception, or if you want me to treat Looper as Nullable in the whole class. ## Changelog: [INTERNAL] [FIXED] - Migrate MessageQueueThread and MessageQueueThreadImpl to Kotlin Pull Request resolved: https://github.com/facebook/react-native/pull/48652 Test Plan: Screenshot 2025-01-13 at 21 03 00 Reviewed By: tdn120 Differential Revision: D68155120 Pulled By: rshest fbshipit-source-id: 1082a832df5b1d8ee64ca7be26e4b85e79152f88 --- .../ReactAndroid/api/ReactAndroid.api | 14 +- ...QueueThread.java => MessageQueueThread.kt} | 55 ++--- .../bridge/queue/MessageQueueThreadImpl.java | 226 ------------------ .../bridge/queue/MessageQueueThreadImpl.kt | 188 +++++++++++++++ .../common/futures/SimpleSettableFuture.kt | 2 +- 5 files changed, 223 insertions(+), 262 deletions(-) rename packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/{MessageQueueThread.java => MessageQueueThread.kt} (51%) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index a62c25e8c2b..9a414995c9c 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -1664,13 +1664,15 @@ public class com/facebook/react/bridge/queue/MessageQueueThreadHandler : android public fun dispatchMessage (Landroid/os/Message;)V } -public class com/facebook/react/bridge/queue/MessageQueueThreadImpl : com/facebook/react/bridge/queue/MessageQueueThread { +public final class com/facebook/react/bridge/queue/MessageQueueThreadImpl : com/facebook/react/bridge/queue/MessageQueueThread { + public static final field Companion Lcom/facebook/react/bridge/queue/MessageQueueThreadImpl$Companion; + public synthetic fun (Ljava/lang/String;Landroid/os/Looper;Lcom/facebook/react/bridge/queue/QueueThreadExceptionHandler;Lcom/facebook/react/bridge/queue/MessageQueueThreadPerfStats;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun assertIsOnThread ()V public fun assertIsOnThread (Ljava/lang/String;)V public fun callOnQueue (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Future; - public static fun create (Lcom/facebook/react/bridge/queue/MessageQueueThreadSpec;Lcom/facebook/react/bridge/queue/QueueThreadExceptionHandler;)Lcom/facebook/react/bridge/queue/MessageQueueThreadImpl; - public fun getLooper ()Landroid/os/Looper; - public fun getName ()Ljava/lang/String; + public static final fun create (Lcom/facebook/react/bridge/queue/MessageQueueThreadSpec;Lcom/facebook/react/bridge/queue/QueueThreadExceptionHandler;)Lcom/facebook/react/bridge/queue/MessageQueueThreadImpl; + public final fun getLooper ()Landroid/os/Looper; + public final fun getName ()Ljava/lang/String; public fun getPerfStats ()Lcom/facebook/react/bridge/queue/MessageQueueThreadPerfStats; public fun isIdle ()Z public fun isOnThread ()Z @@ -1679,6 +1681,10 @@ public class com/facebook/react/bridge/queue/MessageQueueThreadImpl : com/facebo public fun runOnQueue (Ljava/lang/Runnable;)Z } +public final class com/facebook/react/bridge/queue/MessageQueueThreadImpl$Companion { + public final fun create (Lcom/facebook/react/bridge/queue/MessageQueueThreadSpec;Lcom/facebook/react/bridge/queue/QueueThreadExceptionHandler;)Lcom/facebook/react/bridge/queue/MessageQueueThreadImpl; +} + public class com/facebook/react/bridge/queue/MessageQueueThreadPerfStats { public field cpuTime J public field wallTime J diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.kt similarity index 51% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.kt index 5656ad58c49..b2d7d937156 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThread.kt @@ -5,74 +5,67 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.bridge.queue; +package com.facebook.react.bridge.queue -import androidx.annotation.Nullable; -import com.facebook.proguard.annotations.DoNotStrip; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; +import com.facebook.proguard.annotations.DoNotStripAny +import com.facebook.react.bridge.AssertionException +import java.util.concurrent.Callable +import java.util.concurrent.Future /** Encapsulates a Thread that can accept Runnables. */ -@DoNotStrip +@DoNotStripAny public interface MessageQueueThread { /** * Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even * if it is being submitted from the same queue Thread. */ - @DoNotStrip - boolean runOnQueue(Runnable runnable); + public fun runOnQueue(runnable: Runnable): Boolean /** * Runs the given Callable on this Thread. It will be submitted to the end of the event queue even * if it is being submitted from the same queue Thread. */ - @DoNotStrip - Future callOnQueue(final Callable callable); + public fun callOnQueue(callable: Callable): Future /** - * @return whether the current Thread is also the Thread associated with this MessageQueueThread. + * Tells whether the current Thread is also the Thread associated with this MessageQueueThread. */ - @DoNotStrip - boolean isOnThread(); + public fun isOnThread(): Boolean /** - * Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an {@link - * AssertionError}) if the assertion fails. + * Asserts [isOnThread], throwing a [AssertionException] (NOT an [AssertionError]) if the + * assertion fails. */ - @DoNotStrip - void assertIsOnThread(); + public fun assertIsOnThread() /** - * Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an {@link - * AssertionError}) if the assertion fails. The given message is appended to the error. + * Asserts [isOnThread], throwing a [AssertionException] (NOT an [AssertionError]) if the + * assertion fails. The given message is appended to the error. */ - @DoNotStrip - void assertIsOnThread(String message); + public fun assertIsOnThread(message: String) /** * Quits this MessageQueueThread. If called from this MessageQueueThread, this will be the last * thing the thread runs. If called from a separate thread, this will block until the thread can * be quit and joined. */ - @DoNotStrip - void quitSynchronous(); + public fun quitSynchronous() /** * Returns the perf counters taken when the framework was started. This method is intended to be * used for instrumentation purposes. */ - @Nullable - @DoNotStrip - MessageQueueThreadPerfStats getPerfStats(); + public fun getPerfStats(): MessageQueueThreadPerfStats? /** * Resets the perf counters. This is useful if the RN threads are being re-used. This method is * intended to be used for instrumentation purposes. */ - @DoNotStrip - void resetPerfStats(); + public fun resetPerfStats() - /** Returns true if the message queue is idle */ - @DoNotStrip - boolean isIdle(); + /** + * Resets the perf counters. This is useful if the RN threads are being re-used. This method is + * intended to be used for instrumentation purposes. + */ + public fun isIdle(): Boolean } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java deleted file mode 100644 index b7e39566ac5..00000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.bridge.queue; - -import android.os.Looper; -import android.os.Process; -import android.os.SystemClock; -import android.util.Pair; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.proguard.annotations.DoNotStrip; -import com.facebook.react.bridge.AssertionException; -import com.facebook.react.bridge.SoftAssertions; -import com.facebook.react.common.ReactConstants; -import com.facebook.react.common.futures.SimpleSettableFuture; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; - -/** Encapsulates a Thread that has a {@link Looper} running on it that can accept Runnables. */ -@DoNotStrip -public class MessageQueueThreadImpl implements MessageQueueThread { - - private final String mName; - private final Looper mLooper; - private final MessageQueueThreadHandler mHandler; - private final String mAssertionErrorMessage; - private final @Nullable MessageQueueThreadPerfStats mPerfStats; - private volatile boolean mIsFinished = false; - - private MessageQueueThreadImpl( - String name, Looper looper, QueueThreadExceptionHandler exceptionHandler) { - this(name, looper, exceptionHandler, null); - } - - private MessageQueueThreadImpl( - String name, - Looper looper, - QueueThreadExceptionHandler exceptionHandler, - @Nullable MessageQueueThreadPerfStats stats) { - mName = name; - mLooper = looper; - mHandler = new MessageQueueThreadHandler(looper, exceptionHandler); - mPerfStats = stats; - mAssertionErrorMessage = "Expected to be called from the '" + getName() + "' thread!"; - } - - /** - * Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even - * if it is being submitted from the same queue Thread. - */ - @DoNotStrip - @Override - public boolean runOnQueue(Runnable runnable) { - if (mIsFinished) { - FLog.w( - ReactConstants.TAG, - "Tried to enqueue runnable on already finished thread: '" - + getName() - + "... dropping Runnable."); - return false; - } - mHandler.post(runnable); - return true; - } - - @DoNotStrip - @Override - public Future callOnQueue(final Callable callable) { - final SimpleSettableFuture future = new SimpleSettableFuture<>(); - runOnQueue( - () -> { - try { - future.set(callable.call()); - } catch (Exception e) { - future.setException(e); - } - }); - return future; - } - - /** - * @return whether the current Thread is also the Thread associated with this MessageQueueThread. - */ - @DoNotStrip - @Override - public boolean isOnThread() { - return mLooper.getThread() == Thread.currentThread(); - } - - /** - * Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an {@link - * AssertionError}) if the assertion fails. - */ - @DoNotStrip - @Override - public void assertIsOnThread() { - SoftAssertions.assertCondition(isOnThread(), mAssertionErrorMessage); - } - - /** - * Asserts {@link #isOnThread()}, throwing a {@link AssertionException} (NOT an {@link - * AssertionError}) if the assertion fails. - */ - @DoNotStrip - @Override - public void assertIsOnThread(String message) { - SoftAssertions.assertCondition( - isOnThread(), - new StringBuilder().append(mAssertionErrorMessage).append(" ").append(message).toString()); - } - - /** - * Quits this queue's Looper. If that Looper was running on a different Thread than the current - * Thread, also waits for the last message being processed to finish and the Thread to die. - */ - @DoNotStrip - @Override - public void quitSynchronous() { - mIsFinished = true; - mLooper.quit(); - if (mLooper.getThread() != Thread.currentThread()) { - try { - mLooper.getThread().join(); - } catch (InterruptedException e) { - throw new RuntimeException("Got interrupted waiting to join thread " + mName); - } - } - } - - @Nullable - @DoNotStrip - @Override - public MessageQueueThreadPerfStats getPerfStats() { - return mPerfStats; - } - - @DoNotStrip - @Override - public void resetPerfStats() { - assignToPerfStats(mPerfStats, -1, -1); - runOnQueue( - () -> { - long wallTime = SystemClock.uptimeMillis(); - long cpuTime = SystemClock.currentThreadTimeMillis(); - assignToPerfStats(mPerfStats, wallTime, cpuTime); - }); - } - - @DoNotStrip - @Override - public boolean isIdle() { - return mLooper.getQueue().isIdle(); - } - - private static void assignToPerfStats( - @Nullable MessageQueueThreadPerfStats stats, long wall, long cpu) { - if (stats == null) { - return; - } - stats.wallTime = wall; - stats.cpuTime = cpu; - } - - public Looper getLooper() { - return mLooper; - } - - public String getName() { - return mName; - } - - public static MessageQueueThreadImpl create( - MessageQueueThreadSpec spec, QueueThreadExceptionHandler exceptionHandler) { - switch (spec.getThreadType()) { - case MAIN_UI: - return createForMainThread(spec.getName(), exceptionHandler); - case NEW_BACKGROUND: - return startNewBackgroundThread(spec.getName(), spec.getStackSize(), exceptionHandler); - default: - throw new RuntimeException("Unknown thread type: " + spec.getThreadType()); - } - } - - /** - * @return a MessageQueueThreadImpl corresponding to Android's main UI thread. - */ - private static MessageQueueThreadImpl createForMainThread( - String name, QueueThreadExceptionHandler exceptionHandler) { - return new MessageQueueThreadImpl(name, Looper.getMainLooper(), exceptionHandler); - } - - /** - * Creates and starts a new MessageQueueThreadImpl encapsulating a new Thread with a new Looper - * running on it. Give it a name for easier debugging and optionally a suggested stack size. When - * this method exits, the new MessageQueueThreadImpl is ready to receive events. - */ - private static MessageQueueThreadImpl startNewBackgroundThread( - final String name, long stackSize, QueueThreadExceptionHandler exceptionHandler) { - final SimpleSettableFuture> dataFuture = - new SimpleSettableFuture<>(); - Thread bgThread = - new Thread( - null, - () -> { - Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); - Looper.prepare(); - MessageQueueThreadPerfStats stats = new MessageQueueThreadPerfStats(); - long wallTime = SystemClock.uptimeMillis(); - long cpuTime = SystemClock.currentThreadTimeMillis(); - assignToPerfStats(stats, wallTime, cpuTime); - dataFuture.set(new Pair<>(Looper.myLooper(), stats)); - Looper.loop(); - }, - "mqt_" + name, - stackSize); - bgThread.start(); - - Pair pair = dataFuture.getOrThrow(); - return new MessageQueueThreadImpl(name, pair.first, exceptionHandler, pair.second); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.kt new file mode 100644 index 00000000000..d16071ac032 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/MessageQueueThreadImpl.kt @@ -0,0 +1,188 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.bridge.queue + +import android.os.Looper +import android.os.Process +import android.os.SystemClock +import android.util.Pair +import com.facebook.common.logging.FLog +import com.facebook.proguard.annotations.DoNotStripAny +import com.facebook.react.bridge.AssertionException +import com.facebook.react.bridge.SoftAssertions +import com.facebook.react.bridge.queue.MessageQueueThreadSpec.ThreadType +import com.facebook.react.common.ReactConstants +import com.facebook.react.common.futures.SimpleSettableFuture +import java.util.concurrent.Callable +import java.util.concurrent.Future +import kotlin.concurrent.Volatile + +/** Encapsulates a Thread that has a [Looper] running on it that can accept Runnables. */ +@DoNotStripAny +public class MessageQueueThreadImpl +private constructor( + public val name: String, + public val looper: Looper, + exceptionHandler: QueueThreadExceptionHandler, + private val stats: MessageQueueThreadPerfStats? = null +) : MessageQueueThread { + private val handler = MessageQueueThreadHandler(looper, exceptionHandler) + private val assertionErrorMessage = "Expected to be called from the '$name' thread!" + + @Volatile private var isFinished = false + + /** + * Runs the given Runnable on this Thread. It will be submitted to the end of the event queue even + * if it is being submitted from the same queue Thread. + */ + public override fun runOnQueue(runnable: Runnable): Boolean { + if (isFinished) { + FLog.w( + ReactConstants.TAG, + "Tried to enqueue runnable on already finished thread: '$name... dropping Runnable.") + return false + } + handler.post(runnable) + return true + } + + public override fun callOnQueue(callable: Callable): Future { + val future = SimpleSettableFuture() + runOnQueue { + try { + future.set(callable.call()) + } catch (e: Exception) { + future.setException(e) + } + } + return future + } + + /** + * @return whether the current Thread is also the Thread associated with this MessageQueueThread. + */ + override fun isOnThread(): Boolean = looper.thread === Thread.currentThread() + + /** + * Asserts [isOnThread], throwing a [AssertionException] (NOT an [AssertionError]) if the + * assertion fails. + */ + @Throws(AssertionException::class) + override fun assertIsOnThread() { + SoftAssertions.assertCondition(isOnThread(), assertionErrorMessage) + } + + /** + * Asserts [isOnThread], throwing a [AssertionException] (NOT an [AssertionError]) if the + * assertion fails. + */ + @Throws(AssertionException::class) + public override fun assertIsOnThread(message: String) { + SoftAssertions.assertCondition( + isOnThread(), + StringBuilder().append(assertionErrorMessage).append(" ").append(message).toString()) + } + + /** + * Quits this queue's Looper. If that Looper was running on a different Thread than the current + * Thread, also waits for the last message being processed to finish and the Thread to die. + */ + @Throws(RuntimeException::class) + override fun quitSynchronous() { + isFinished = true + looper.quit() + if (looper.thread !== Thread.currentThread()) { + try { + looper.thread.join() + } catch (e: InterruptedException) { + throw RuntimeException("Got interrupted waiting to join thread $name") + } + } + } + + override fun getPerfStats(): MessageQueueThreadPerfStats? = stats + + override fun resetPerfStats() { + assignToPerfStats(stats, -1, -1) + runOnQueue { + val wallTime = SystemClock.uptimeMillis() + val cpuTime = SystemClock.currentThreadTimeMillis() + assignToPerfStats(stats, wallTime, cpuTime) + } + } + + public override fun isIdle(): Boolean = looper.queue.isIdle + + public companion object { + private fun assignToPerfStats(stats: MessageQueueThreadPerfStats?, wall: Long, cpu: Long) { + stats?.let { s -> + s.wallTime = wall + s.cpuTime = cpu + } + } + + @JvmStatic + @Throws(RuntimeException::class) + public fun create( + spec: MessageQueueThreadSpec, + exceptionHandler: QueueThreadExceptionHandler + ): MessageQueueThreadImpl { + return when (spec.threadType) { + ThreadType.MAIN_UI -> createForMainThread(spec.name, exceptionHandler) + + ThreadType.NEW_BACKGROUND -> + startNewBackgroundThread(spec.name, spec.stackSize, exceptionHandler) + + else -> throw RuntimeException("Unknown thread type: " + spec.threadType) + } + } + + /** Returns a MessageQueueThreadImpl corresponding to Android's main UI thread. */ + private fun createForMainThread( + name: String, + exceptionHandler: QueueThreadExceptionHandler + ): MessageQueueThreadImpl = + MessageQueueThreadImpl(name, Looper.getMainLooper(), exceptionHandler) + + /** + * Creates and starts a new MessageQueueThreadImpl encapsulating a new Thread with a new Looper + * running on it. Give it a name for easier debugging and optionally a suggested stack size. + * When this method exits, the new MessageQueueThreadImpl is ready to receive events. throws a + * Runtime exception if there was no looper for current thread or looper and stats couldn't be + * made + */ + @Throws(RuntimeException::class) + private fun startNewBackgroundThread( + name: String, + stackSize: Long, + exceptionHandler: QueueThreadExceptionHandler + ): MessageQueueThreadImpl { + val dataFuture = SimpleSettableFuture>() + val bgThread = + Thread( + null, + { + Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY) + Looper.prepare() + val stats = MessageQueueThreadPerfStats() + val wallTime = SystemClock.uptimeMillis() + val cpuTime = SystemClock.currentThreadTimeMillis() + assignToPerfStats(stats, wallTime, cpuTime) + dataFuture.set(Pair(Looper.myLooper(), stats)) + Looper.loop() + }, + "mqt_$name", + stackSize) + bgThread.start() + + val pair = dataFuture.getOrThrow() + val looper = pair?.first ?: throw RuntimeException("Looper not found for thread") + return MessageQueueThreadImpl(name, looper, exceptionHandler, pair.second) + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.kt index 95c093e6670..3376f4d2a2d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/futures/SimpleSettableFuture.kt @@ -17,7 +17,7 @@ import java.util.concurrent.TimeoutException * A super simple Future-like class that can safely notify another Thread when a value is ready. * Does not support canceling. */ -internal class SimpleSettableFuture : Future { +internal class SimpleSettableFuture : Future { private val readyLatch = CountDownLatch(1) private var result: T? = null