blob: 577cdbf07e80710993dfd19f559e9f47795885f7 [file] [log] [blame]
Sorin Basca18049482022-03-01 11:49:02 +00001/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import java.lang.invoke.VarHandle;
18import java.lang.invoke.MethodHandles;
19import java.time.Duration;
20import java.util.concurrent.atomic.AtomicInteger;
21import java.util.function.Consumer;
22
23/**
24 * Runs tests to validate the concurrency guarantees of VarHandle.
25 *
26 * The tests involve having a lot of tasks and significantly fewer threads. The tasks are stored on
27 * a queue and each thread tries to grab a task from the queue using operations like
28 * VarHandle.compareAndSet(). If the operation works as specified, then each task would only be
29 * handled in a single thread, exactly once.
30 *
31 * The tasks just add atomically a specified integer to a total. If the total is different from the
32 * expected one, then either some tasks were run multiple times (on multiple threads), or some task
33 * were not run at all (skipped by all threads).
34 */
35public class Main {
36 private static final VarHandle QA;
37 static {
38 QA = MethodHandles.arrayElementVarHandle(TestTask[].class);
39 }
40
41 private static final int TASK_COUNT = 10000;
Sorin Basca6bc83dd2022-04-26 11:44:55 +000042 private static final int THREAD_COUNT = 20;
Sorin Basca18049482022-03-01 11:49:02 +000043 /* Each test may need several retries before a concurrent failure is seen. In the past, for a
44 * known bug, between 5 and 10 retries were sufficient. Use RETRIES to configure how many
45 * iterations to retry for each test scenario. However, to avoid the test running for too long,
46 * for example with gcstress, set a cap duration in MAX_RETRIES_DURATION. With this at least one
47 * iteration would run, but there could be fewer retries if each of them takes too long. */
48 private static final int RETRIES = 50;
Sorin Basca6b4805b2022-06-22 17:31:00 +010049 // b/235431387: timeout reduced from 1 minute
50 private static final Duration MAX_RETRIES_DURATION = Duration.ofSeconds(15);
Sorin Basca18049482022-03-01 11:49:02 +000051
52 public static void main(String[] args) throws Throwable {
53 testConcurrentProcessing(new CompareAndExchangeRunnerFactory(), "compareAndExchange");
54 testConcurrentProcessing(new CompareAndSetRunnerFactory(), "compareAndSet");
55 testConcurrentProcessing(new WeakCompareAndSetRunnerFactory(), "weakCompareAndSet");
56 }
57
58 private static void testConcurrentProcessing(RunnerFactory factory,
59 String testName) throws Throwable {
60 final Duration startTs = Duration.ofNanos(System.nanoTime());
61 final Duration endTs = startTs.plus(MAX_RETRIES_DURATION);
62 for (int i = 0; i < RETRIES; ++i) {
63 concurrentProcessingTestIteration(factory, i, testName);
64 Duration now = Duration.ofNanos(System.nanoTime());
65 if (0 < now.compareTo(endTs)) {
Sorin Basca18049482022-03-01 11:49:02 +000066 break;
67 }
68 }
69 }
70
71 private static void concurrentProcessingTestIteration(RunnerFactory factory,
72 int iteration, String testName) throws Throwable {
73 final TestTask[] tasks = new TestTask[TASK_COUNT];
74 final AtomicInteger result = new AtomicInteger();
75
76 for (int i = 0; i < TASK_COUNT; ++i) {
77 tasks[i] = new TestTask(Integer.valueOf(i+1), result::addAndGet);
78 }
79
80 Thread[] threads = new Thread[THREAD_COUNT];
81 for (int i = 0; i < THREAD_COUNT; ++i) {
82 threads[i] = factory.createRunner(tasks);
83 }
84
85 for (int i = 0; i < THREAD_COUNT; ++i) {
86 threads[i].start();
87 }
88
89 for (int i = 0; i < THREAD_COUNT; ++i) {
90 threads[i].join();
91 }
92
93 check(result.get(), TASK_COUNT * (TASK_COUNT + 1) / 2,
94 testName + " test result not as expected", iteration);
95 }
96
97 /**
98 * Processes the task queue until there are no tasks left.
99 *
100 * The actual task-grabbing mechanism is implemented in subclasses through grabTask(). This allows
101 * testing various mechanisms, like compareAndSet() and compareAndExchange().
102 */
103 private static abstract class TaskRunner extends Thread {
104
105 protected final TestTask[] tasks;
106
107 TaskRunner(TestTask[] tasks) {
108 this.tasks = tasks;
109 }
110
111 @Override
112 public void run() {
113 int i = 0;
114 while (i < TASK_COUNT) {
115 TestTask t = (TestTask) QA.get(tasks, i);
116 if (t == null) {
117 ++i;
118 continue;
119 }
120 if (!grabTask(t, i)) {
121 continue;
122 }
123 ++i;
124 VarHandle.releaseFence();
125 t.exec();
126 }
127 }
128
129 /**
130 * Grabs the next task from the queue in an atomic way.
131 *
132 * Once a task is retrieved successfully, the queue should no longer hold a reference to it.
133 * This would be done, for example, by swapping the task with a null value.
134 *
135 * @param t The task to get from the queue
136 * @param i The index where the task is found
137 *
138 * @return {@code true} if the task has been retrieved and is not available to any other
139 * threads. Otherwise {@code false}. If {@code false} is returned, then either the task was no
140 * longer present on the queue due to another thread grabbing it, or, in case of spurious
141 * failure, the task is still available and no other thread managed to grab it.
142 */
143 protected abstract boolean grabTask(TestTask t, int i);
144 }
145
146 private static class TaskRunnerWithCompareAndExchange extends TaskRunner {
147
148 TaskRunnerWithCompareAndExchange(TestTask[] tasks) {
149 super(tasks);
150 }
151
152 @Override
153 protected boolean grabTask(TestTask t, int i) {
154 return (t == QA.compareAndExchange(tasks, i, t, null));
155 }
156 }
157
158 private static class TaskRunnerWithCompareAndSet extends TaskRunner {
159
160 TaskRunnerWithCompareAndSet(TestTask[] tasks) {
161 super(tasks);
162 }
163
164 @Override
165 protected boolean grabTask(TestTask t, int i) {
166 return QA.compareAndSet(tasks, i, t, null);
167 }
168 }
169
170 private static class TaskRunnerWithWeakCompareAndSet extends TaskRunner {
171
172 TaskRunnerWithWeakCompareAndSet(TestTask[] tasks) {
173 super(tasks);
174 }
175
176 @Override
177 protected boolean grabTask(TestTask t, int i) {
178 return QA.weakCompareAndSet(tasks, i, t, null);
179 }
180 }
181
182
183 private interface RunnerFactory {
184 Thread createRunner(TestTask[] tasks);
185 }
186
187 private static class CompareAndExchangeRunnerFactory implements RunnerFactory {
188 @Override
189 public Thread createRunner(TestTask[] tasks) {
190 return new TaskRunnerWithCompareAndExchange(tasks);
191 }
192 }
193
194 private static class CompareAndSetRunnerFactory implements RunnerFactory {
195 @Override
196 public Thread createRunner(TestTask[] tasks) {
197 return new TaskRunnerWithCompareAndSet(tasks);
198 }
199 }
200
201 private static class WeakCompareAndSetRunnerFactory implements RunnerFactory {
202 @Override
203 public Thread createRunner(TestTask[] tasks) {
204 return new TaskRunnerWithWeakCompareAndSet(tasks);
205 }
206 }
207
208 private static class TestTask {
209 private final Integer ord;
210 private final Consumer<Integer> action;
211
212 TestTask(Integer ord, Consumer<Integer> action) {
213 this.ord = ord;
214 this.action = action;
215 }
216
217 public void exec() {
218 action.accept(ord);
219 }
220 }
221
222 private static void check(int actual, int expected, String msg, int iteration) {
223 if (actual != expected) {
224 System.err.println(String.format("[iteration %d] %s : %d != %d",
225 iteration, msg, actual, expected));
226 System.exit(1);
227 }
228 }
229}