Skip to content

Commit dd5c646

Browse files
Improve performance of NewThreadWorker.tryEnableCancelPolicy().
Disable search for ScheduledExecutorService.setRemoveOnCancelPolicy() on Android API < 21
1 parent 3494c00 commit dd5c646

File tree

3 files changed

+186
-29
lines changed

3 files changed

+186
-29
lines changed

src/main/java/rx/internal/schedulers/NewThreadWorker.java

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import rx.plugins.*;
2828
import rx.subscriptions.*;
2929

30+
import static rx.internal.util.PlatformDependent.ANDROID_API_VERSION_IS_NOT_ANDROID;
31+
3032
/**
3133
* @warn class description missing
3234
*/
@@ -39,17 +41,25 @@ public class NewThreadWorker extends Scheduler.Worker implements Subscription {
3941
/** Force the use of purge (true/false). */
4042
private static final String PURGE_FORCE_KEY = "rx.scheduler.jdk6.purge-force";
4143
private static final String PURGE_THREAD_PREFIX = "RxSchedulerPurge-";
42-
/** Forces the use of purge even if setRemoveOnCancelPolicy is available. */
43-
private static final boolean PURGE_FORCE;
44+
private static final boolean SHOULD_TRY_ENABLE_CANCEL_POLICY;
4445
/** The purge frequency in milliseconds. */
4546
public static final int PURGE_FREQUENCY;
4647
private static final ConcurrentHashMap<ScheduledThreadPoolExecutor, ScheduledThreadPoolExecutor> EXECUTORS;
4748
private static final AtomicReference<ScheduledExecutorService> PURGE;
4849
static {
4950
EXECUTORS = new ConcurrentHashMap<ScheduledThreadPoolExecutor, ScheduledThreadPoolExecutor>();
5051
PURGE = new AtomicReference<ScheduledExecutorService>();
51-
PURGE_FORCE = Boolean.getBoolean(PURGE_FORCE_KEY);
5252
PURGE_FREQUENCY = Integer.getInteger(FREQUENCY_KEY, 1000);
53+
54+
// Forces the use of purge even if setRemoveOnCancelPolicy is available
55+
final boolean purgeForce = Boolean.getBoolean(PURGE_FORCE_KEY);
56+
57+
final int androidApiVersion = PlatformDependent.getAndroidApiVersion();
58+
59+
// According to http://developer.android.com/reference/java/util/concurrent/ScheduledThreadPoolExecutor.html#setRemoveOnCancelPolicy(boolean)
60+
// setRemoveOnCancelPolicy available since Android API 21
61+
SHOULD_TRY_ENABLE_CANCEL_POLICY = !purgeForce
62+
&& (androidApiVersion == ANDROID_API_VERSION_IS_NOT_ANDROID || androidApiVersion >= 21);
5363
}
5464
/**
5565
* Registers the given executor service and starts the purge thread if not already started.
@@ -85,6 +95,7 @@ public void run() {
8595
public static void deregisterExecutor(ScheduledExecutorService service) {
8696
EXECUTORS.remove(service);
8797
}
98+
8899
/** Purges each registered executor and eagerly evicts shutdown executors. */
89100
static void purgeExecutors() {
90101
try {
@@ -102,32 +113,89 @@ static void purgeExecutors() {
102113
RxJavaPlugins.getInstance().getErrorHandler().handleError(t);
103114
}
104115
}
105-
106-
/**
116+
117+
/**
118+
* Improves performance of {@link #tryEnableCancelPolicy(ScheduledExecutorService)}.
119+
* Also, it works even for inheritance: {@link Method} of base class can be invoked on the instance of child class.
120+
*/
121+
private static volatile Object cachedSetRemoveOnCancelPolicyMethod;
122+
123+
/**
124+
* Possible value of {@link #cachedSetRemoveOnCancelPolicyMethod} which means that cancel policy is not supported.
125+
*/
126+
private static final Object SET_REMOVE_ON_CANCEL_POLICY_METHOD_NOT_SUPPORTED = new Object();
127+
128+
/**
107129
* Tries to enable the Java 7+ setRemoveOnCancelPolicy.
108130
* <p>{@code public} visibility reason: called from other package(s) within RxJava.
109131
* If the method returns false, the {@link #registerExecutor(ScheduledThreadPoolExecutor)} may
110132
* be called to enable the backup option of purging the executors.
111-
* @param exec the executor to call setRemoveOnCaneclPolicy if available.
133+
* @param executor the executor to call setRemoveOnCaneclPolicy if available.
112134
* @return true if the policy was successfully enabled
113135
*/
114-
public static boolean tryEnableCancelPolicy(ScheduledExecutorService exec) {
115-
if (!PURGE_FORCE) {
116-
for (Method m : exec.getClass().getMethods()) {
117-
if (m.getName().equals("setRemoveOnCancelPolicy")
118-
&& m.getParameterTypes().length == 1
119-
&& m.getParameterTypes()[0] == Boolean.TYPE) {
120-
try {
121-
m.invoke(exec, true);
122-
return true;
123-
} catch (Exception ex) {
124-
RxJavaPlugins.getInstance().getErrorHandler().handleError(ex);
125-
}
136+
public static boolean tryEnableCancelPolicy(ScheduledExecutorService executor) {
137+
if (SHOULD_TRY_ENABLE_CANCEL_POLICY) {
138+
final boolean isInstanceOfScheduledThreadPoolExecutor = executor instanceof ScheduledThreadPoolExecutor;
139+
140+
final Method methodToCall;
141+
142+
if (isInstanceOfScheduledThreadPoolExecutor) {
143+
final Object localSetRemoveOnCancelPolicyMethod = cachedSetRemoveOnCancelPolicyMethod;
144+
145+
if (localSetRemoveOnCancelPolicyMethod == SET_REMOVE_ON_CANCEL_POLICY_METHOD_NOT_SUPPORTED) {
146+
return false;
147+
}
148+
149+
if (localSetRemoveOnCancelPolicyMethod == null) {
150+
Method method = findSetRemoveOnCancelPolicyMethod(executor);
151+
152+
cachedSetRemoveOnCancelPolicyMethod = method != null
153+
? method
154+
: SET_REMOVE_ON_CANCEL_POLICY_METHOD_NOT_SUPPORTED;
155+
156+
methodToCall = method;
157+
} else {
158+
methodToCall = (Method) localSetRemoveOnCancelPolicyMethod;
159+
}
160+
} else {
161+
methodToCall = findSetRemoveOnCancelPolicyMethod(executor);
162+
}
163+
164+
if (methodToCall != null) {
165+
try {
166+
methodToCall.invoke(executor, true);
167+
return true;
168+
} catch (Exception e) {
169+
RxJavaPlugins.getInstance().getErrorHandler().handleError(e);
126170
}
127171
}
128172
}
173+
129174
return false;
130175
}
176+
177+
/**
178+
* Tries to find {@code "setRemoveOnCancelPolicy(boolean)"} method in the class of passed executor.
179+
*
180+
* @param executor whose class will be used to search for required method.
181+
* @return {@code "setRemoveOnCancelPolicy(boolean)"} {@link Method}
182+
* or {@code null} if required {@link Method} was not found.
183+
*/
184+
static Method findSetRemoveOnCancelPolicyMethod(ScheduledExecutorService executor) {
185+
// The reason for the loop is to avoid NoSuchMethodException being thrown on JDK 6
186+
// which is more costly than looping through ~70 methods.
187+
for (final Method method : executor.getClass().getMethods()) {
188+
if (method.getName().equals("setRemoveOnCancelPolicy")) {
189+
final Class<?>[] parameterTypes = method.getParameterTypes();
190+
191+
if (parameterTypes.length == 1 && parameterTypes[0] == Boolean.TYPE) {
192+
return method;
193+
}
194+
}
195+
}
196+
197+
return null;
198+
}
131199

132200
/* package */
133201
public NewThreadWorker(ThreadFactory threadFactory) {

src/main/java/rx/internal/util/PlatformDependent.java

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,55 @@
2020

2121
/**
2222
* Allow platform dependent logic such as checks for Android.
23-
*
23+
*
2424
* Modeled after Netty with some code copy/pasted from: https://github.com/netty/netty/blob/master/common/src/main/java/io/netty/util/internal/PlatformDependent.java
2525
*/
2626
public final class PlatformDependent {
2727

28-
private static final boolean IS_ANDROID = isAndroid0();
28+
/**
29+
* Possible value of {@link #getAndroidApiVersion()} which means that the current platform is not Android.
30+
*/
31+
public static final int ANDROID_API_VERSION_IS_NOT_ANDROID = 0;
32+
33+
private static final int ANDROID_API_VERSION = resolveAndroidApiVersion();
34+
35+
private static final boolean IS_ANDROID = ANDROID_API_VERSION != ANDROID_API_VERSION_IS_NOT_ANDROID;
2936

3037
/**
31-
* Returns {@code true} if and only if the current platform is Android
38+
* Returns {@code true} if and only if the current platform is Android.
3239
*/
3340
public static boolean isAndroid() {
3441
return IS_ANDROID;
3542
}
3643

37-
private static boolean isAndroid0() {
38-
boolean android;
44+
/**
45+
* Returns version of Android API.
46+
*
47+
* @return version of Android API or {@link #ANDROID_API_VERSION_IS_NOT_ANDROID } if version
48+
* can not be resolved or if current platform is not Android.
49+
*/
50+
public static int getAndroidApiVersion() {
51+
return ANDROID_API_VERSION;
52+
}
53+
54+
/**
55+
* Resolves version of Android API.
56+
*
57+
* @return version of Android API or {@link #ANDROID_API_VERSION_IS_NOT_ANDROID} if version can not be resolved
58+
* or if the current platform is not Android.
59+
* @see <a href="http://developer.android.com/reference/android/os/Build.VERSION.html#SDK_INT">Documentation</a>
60+
*/
61+
private static int resolveAndroidApiVersion() {
3962
try {
40-
Class.forName("android.app.Application", false, getSystemClassLoader());
41-
android = true;
63+
return (Integer) Class
64+
.forName("android.os.Build$VERSION", true, getSystemClassLoader())
65+
.getField("SDK_INT")
66+
.get(null);
4267
} catch (Exception e) {
43-
// Failed to load the class uniquely available in Android.
44-
android = false;
68+
// Can not resolve version of Android API, maybe current platform is not Android
69+
// or API of resolving current Version of Android API has changed in some release of Android
70+
return ANDROID_API_VERSION_IS_NOT_ANDROID;
4571
}
46-
47-
return android;
4872
}
4973

5074
/**
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package rx.internal.schedulers;
2+
3+
import org.junit.Test;
4+
5+
import java.lang.reflect.Field;
6+
import java.lang.reflect.Method;
7+
import java.util.concurrent.ScheduledExecutorService;
8+
import java.util.concurrent.ScheduledThreadPoolExecutor;
9+
import java.util.concurrent.atomic.AtomicBoolean;
10+
11+
import static java.lang.reflect.Modifier.FINAL;
12+
import static org.junit.Assert.*;
13+
import static org.mockito.Mockito.*;
14+
15+
public class NewThreadWorkerTest {
16+
17+
@Test
18+
public void findSetRemoveOnCancelPolicyMethodShouldFindMethod() {
19+
ScheduledExecutorService executor = spy(new ScheduledThreadPoolExecutor(1));
20+
Method setRemoveOnCancelPolicyMethod = NewThreadWorker.findSetRemoveOnCancelPolicyMethod(executor);
21+
22+
assertNotNull(setRemoveOnCancelPolicyMethod);
23+
assertEquals("setRemoveOnCancelPolicy", setRemoveOnCancelPolicyMethod.getName());
24+
assertEquals(1, setRemoveOnCancelPolicyMethod.getParameterTypes().length);
25+
assertEquals(Boolean.TYPE, setRemoveOnCancelPolicyMethod.getParameterTypes()[0]);
26+
verifyZeroInteractions(executor);
27+
}
28+
29+
@Test
30+
public void findSetRemoveOnCancelPolicyMethodShouldNotFindMethod() {
31+
ScheduledExecutorService executor = mock(ScheduledExecutorService.class);
32+
33+
Method setRemoveOnCancelPolicyMethod = NewThreadWorker.findSetRemoveOnCancelPolicyMethod(executor);
34+
assertNull(setRemoveOnCancelPolicyMethod);
35+
verifyZeroInteractions(executor);
36+
}
37+
38+
private static abstract class ScheduledExecutorServiceWithSetRemoveOnCancelPolicy implements ScheduledExecutorService {
39+
// Just declaration of required method to allow run tests on JDK 6
40+
public void setRemoveOnCancelPolicy(@SuppressWarnings("UnusedParameters") boolean value) {}
41+
}
42+
43+
@Test
44+
public void tryEnableCancelPolicyShouldInvokeMethodOnExecutor() {
45+
ScheduledExecutorServiceWithSetRemoveOnCancelPolicy executor
46+
= mock(ScheduledExecutorServiceWithSetRemoveOnCancelPolicy.class);
47+
48+
boolean result = NewThreadWorker.tryEnableCancelPolicy(executor);
49+
50+
assertTrue(result);
51+
verify(executor).setRemoveOnCancelPolicy(true);
52+
verifyNoMoreInteractions(executor);
53+
}
54+
55+
@Test
56+
public void tryEnableCancelPolicyShouldNotInvokeMethodOnExecutor() {
57+
// This executor does not have setRemoveOnCancelPolicy method
58+
ScheduledExecutorService executor = mock(ScheduledExecutorService.class);
59+
60+
boolean result = NewThreadWorker.tryEnableCancelPolicy(executor);
61+
62+
assertFalse(result);
63+
verifyZeroInteractions(executor);
64+
}
65+
}

0 commit comments

Comments
 (0)