Skip to content

Commit d5e5df4

Browse files
ObserveOn with Buffer Size
1 parent e657d22 commit d5e5df4

File tree

4 files changed

+172
-53
lines changed

4 files changed

+172
-53
lines changed

rxjava-core/src/main/java/rx/Observable.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5139,8 +5139,7 @@ public final <R> ConnectableObservable<R> multicast(Subject<? super T, ? extends
51395139
}
51405140

51415141
/**
5142-
* Modify the source Observable so that it asynchronously notifies {@link Observer}s on the
5143-
* specified {@link Scheduler}.
5142+
* Move notifications to the specified {@link Scheduler} one `onNext` at a time.
51445143
* <p>
51455144
* <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/observeOn.png">
51465145
*
@@ -5154,6 +5153,23 @@ public final Observable<T> observeOn(Scheduler scheduler) {
51545153
return lift(new OperatorObserveOn<T>(scheduler));
51555154
}
51565155

5156+
/**
5157+
* Move notifications to the specified {@link Scheduler} asynchronously with a buffer of the given size.
5158+
* <p>
5159+
* <img width="640" src="https://raw.github.com/wiki/Netflix/RxJava/images/rx-operators/observeOn.png">
5160+
*
5161+
* @param scheduler
5162+
* the {@link Scheduler} to notify {@link Observer}s on
5163+
* @param bufferSize
5164+
* that will be rounded up to the next power of 2
5165+
* @return the source Observable modified so that its {@link Observer}s are notified on the
5166+
* specified {@link Scheduler}
5167+
* @see <a href="https://github.com/Netflix/RxJava/wiki/Observable-Utility-Operators#wiki-observeon">RxJava Wiki: observeOn()</a>
5168+
*/
5169+
public final Observable<T> observeOn(Scheduler scheduler, int bufferSize) {
5170+
return lift(new OperatorObserveOn<T>(scheduler, bufferSize));
5171+
}
5172+
51575173
/**
51585174
* Filters the items emitted by an Observable, only emitting those of the specified type.
51595175
* <p>

rxjava-core/src/main/java/rx/operators/OperatorObserveOn.java

Lines changed: 95 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,35 @@
4848
public class OperatorObserveOn<T> implements Operator<T, T> {
4949

5050
private final Scheduler scheduler;
51+
private final int bufferSize;
5152

52-
public OperatorObserveOn(Scheduler scheduler) {
53+
/**
54+
*
55+
* @param scheduler
56+
* @param bufferSize
57+
* that will be rounded up to the next power of 2
58+
*/
59+
public OperatorObserveOn(Scheduler scheduler, int bufferSize) {
5360
this.scheduler = scheduler;
61+
this.bufferSize = roundToNextPowerOfTwoIfNecessary(bufferSize);
62+
}
63+
64+
public OperatorObserveOn(Scheduler scheduler) {
65+
this(scheduler, 1);
66+
}
67+
68+
private static int roundToNextPowerOfTwoIfNecessary(int num) {
69+
if ((num & -num) == num) {
70+
return num;
71+
} else {
72+
int result = 1;
73+
while (num != 0)
74+
{
75+
num >>= 1;
76+
result <<= 1;
77+
}
78+
return result;
79+
}
5480
}
5581

5682
@Override
@@ -87,7 +113,7 @@ private class ObserveOnSubscriber extends Subscriber<T> {
87113
final Subscriber<? super T> observer;
88114
private volatile Scheduler.Inner recursiveScheduler;
89115

90-
private final InterruptibleBlockingQueue queue = new InterruptibleBlockingQueue();
116+
private final InterruptibleBlockingQueue queue = new InterruptibleBlockingQueue(bufferSize);
91117
final AtomicLong counter = new AtomicLong(0);
92118

93119
public ObserveOnSubscriber(Subscriber<? super T> observer) {
@@ -101,9 +127,9 @@ public void onNext(final T t) {
101127
// we want to block for natural back-pressure
102128
// so that the producer waits for each value to be consumed
103129
if (t == null) {
104-
queue.put(NULL_SENTINEL);
130+
queue.addBlocking(NULL_SENTINEL);
105131
} else {
106-
queue.put(t);
132+
queue.addBlocking(t);
107133
}
108134
schedule();
109135
} catch (InterruptedException e) {
@@ -118,7 +144,7 @@ public void onCompleted() {
118144
try {
119145
// we want to block for natural back-pressure
120146
// so that the producer waits for each value to be consumed
121-
queue.put(COMPLETE_SENTINEL);
147+
queue.addBlocking(COMPLETE_SENTINEL);
122148
schedule();
123149
} catch (InterruptedException e) {
124150
onError(e);
@@ -130,7 +156,7 @@ public void onError(final Throwable e) {
130156
try {
131157
// we want to block for natural back-pressure
132158
// so that the producer waits for each value to be consumed
133-
queue.put(new ErrorSentinel(e));
159+
queue.addBlocking(new ErrorSentinel(e));
134160
schedule();
135161
} catch (InterruptedException e2) {
136162
// call directly if we can't schedule
@@ -195,57 +221,89 @@ private void pollQueue() {
195221

196222
}
197223

198-
/**
199-
* Same behavior as ArrayBlockingQueue<Object>(1) except that we can interrupt/unsubscribe it.
200-
*/
201224
private class InterruptibleBlockingQueue {
202225

203-
private final Semaphore semaphore = new Semaphore(1);
204-
private volatile Object item;
226+
private final Semaphore semaphore;
205227
private volatile boolean interrupted = false;
206228

207-
public Object poll() {
208-
if (interrupted) {
209-
return null;
210-
}
211-
if (item == null) {
212-
return null;
213-
}
214-
try {
215-
return item;
216-
} finally {
217-
item = null;
218-
semaphore.release();
219-
}
229+
private final Object[] buffer;
230+
231+
private AtomicLong tail = new AtomicLong();
232+
private AtomicLong head = new AtomicLong();
233+
private final int capacity;
234+
private final int mask;
235+
236+
public InterruptibleBlockingQueue(final int size) {
237+
this.semaphore = new Semaphore(size);
238+
this.capacity = size;
239+
this.mask = size - 1;
240+
buffer = new Object[size];
220241
}
221242

222243
/**
223-
* Add an Object, blocking if an item is already in the queue.
224-
*
225-
* @param o
226-
* @throws InterruptedException
244+
* Used to unsubscribe and interrupt the producer if blocked in put()
227245
*/
228-
public void put(Object o) throws InterruptedException {
246+
public void interrupt() {
247+
interrupted = true;
248+
semaphore.release();
249+
}
250+
251+
public void addBlocking(final Object e) throws InterruptedException {
229252
if (interrupted) {
230253
throw new InterruptedException("Interrupted by Unsubscribe");
231254
}
232255
semaphore.acquire();
233256
if (interrupted) {
234257
throw new InterruptedException("Interrupted by Unsubscribe");
235258
}
236-
if (o == null) {
259+
if (e == null) {
237260
throw new IllegalArgumentException("Can not put null");
238261
}
239-
item = o;
262+
263+
if (offer(e)) {
264+
return;
265+
} else {
266+
throw new IllegalStateException("Queue is full");
267+
}
240268
}
241269

242-
/**
243-
* Used to unsubscribe and interrupt the producer if blocked in put()
244-
*/
245-
public void interrupt() {
246-
interrupted = true;
247-
semaphore.release();
270+
private boolean offer(final Object e) {
271+
final long _t = tail.get();
272+
if (_t - head.get() == capacity) {
273+
// queue is full
274+
return false;
275+
}
276+
int index = (int) (_t & mask);
277+
buffer[index] = e;
278+
// move the tail forward
279+
tail.lazySet(_t + 1);
280+
281+
return true;
248282
}
283+
284+
public Object poll() {
285+
if (interrupted) {
286+
return null;
287+
}
288+
final long _h = head.get();
289+
if (tail.get() == _h) {
290+
// nothing available
291+
return null;
292+
}
293+
int index = (int) (_h & mask);
294+
295+
// fetch the item
296+
Object v = buffer[index];
297+
// allow GC to happen
298+
buffer[index] = null;
299+
// increment and signal we're done
300+
head.lazySet(_h + 1);
301+
if (v != null) {
302+
semaphore.release();
303+
}
304+
return v;
305+
}
306+
249307
}
250308

251309
}

rxjava-core/src/perf/java/rx/operators/OperatorObserveOnPerformance.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
public class OperatorObserveOnPerformance extends AbstractPerformanceTester {
1010

11-
private static long reps = 1000000;
11+
private static long reps = 10000;
1212

1313
OperatorObserveOnPerformance() {
1414
super(reps);
@@ -34,10 +34,43 @@ public void call() {
3434
/**
3535
* Observable.from(1L).observeOn()
3636
*
37+
* --- version 0.17.1 => with queue size == 1
38+
*
39+
* Run: 10 - 115,033 ops/sec
40+
* Run: 11 - 118,155 ops/sec
41+
* Run: 12 - 120,526 ops/sec
42+
* Run: 13 - 115,035 ops/sec
43+
* Run: 14 - 116,102 ops/sec
44+
*
45+
* --- version 0.17.1 => with queue size == 16
46+
*
47+
* Run: 10 - 850,412 ops/sec
48+
* Run: 11 - 711,642 ops/sec
49+
* Run: 12 - 788,332 ops/sec
50+
* Run: 13 - 1,064,056 ops/sec
51+
* Run: 14 - 656,857 ops/sec
52+
*
53+
* --- version 0.17.1 => with queue size == 1000000
54+
*
55+
* Run: 10 - 5,162,622 ops/sec
56+
* Run: 11 - 5,271,481 ops/sec
57+
* Run: 12 - 4,442,470 ops/sec
58+
* Run: 13 - 5,149,330 ops/sec
59+
* Run: 14 - 5,146,680 ops/sec
60+
*
61+
* --- version 0.16.1
62+
*
63+
* Run: 10 - 27,098,802 ops/sec
64+
* Run: 11 - 24,204,284 ops/sec
65+
* Run: 12 - 27,208,663 ops/sec
66+
* Run: 13 - 26,879,552 ops/sec
67+
* Run: 14 - 26,658,846 ops/sec
68+
*
69+
*
3770
*/
3871
public long timeObserveOn() {
3972

40-
Observable<Integer> s = Observable.range(1, (int) reps).observeOn(Schedulers.newThread());
73+
Observable<Integer> s = Observable.range(1, (int) reps).observeOn(Schedulers.newThread(), 16);
4174
IntegerSumObserver o = new IntegerSumObserver();
4275
s.subscribe(o);
4376
return o.sum;

rxjava-core/src/test/java/rx/operators/OperatorObserveOnTest.java

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -348,30 +348,35 @@ public void onNext(Integer t) {
348348

349349
@Test
350350
public final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribeNewThread() throws InterruptedException {
351-
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.newThread());
351+
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.newThread(), 1);
352+
}
353+
354+
@Test
355+
public final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribeNewThreadAndBuffer8() throws InterruptedException {
356+
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.newThread(), 8);
352357
}
353358

354359
@Test
355360
public final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribeIO() throws InterruptedException {
356-
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.io());
361+
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.io(), 1);
357362
}
358363

359364
@Test
360365
public final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribeTrampoline() throws InterruptedException {
361-
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.trampoline());
366+
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.trampoline(), 1);
362367
}
363368

364369
@Test
365370
public final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribeTestScheduler() throws InterruptedException {
366-
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.test());
371+
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.test(), 1);
367372
}
368373

369374
@Test
370375
public final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribeComputation() throws InterruptedException {
371-
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.computation());
376+
testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Schedulers.computation(), 1);
372377
}
373378

374-
private final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Scheduler scheduler) throws InterruptedException {
379+
private final void testBackpressureOnFastProducerSlowConsumerWithUnsubscribe(Scheduler scheduler, int bufferSize) throws InterruptedException {
375380
final AtomicInteger countEmitted = new AtomicInteger();
376381
final AtomicInteger countTaken = new AtomicInteger();
377382
int value = Observable.create(new OnSubscribeFunc<Integer>() {
@@ -385,7 +390,7 @@ public Subscription onSubscribe(final Observer<? super Integer> o) {
385390
public void run() {
386391
int i = 1;
387392
while (!s.isUnsubscribed() && i <= 100) {
388-
System.out.println("onNext from fast producer [" + Thread.currentThread() + "]: " + i);
393+
// System.out.println("onNext from fast producer [" + Thread.currentThread() + "]: " + i);
389394
o.onNext(i++);
390395
}
391396
o.onCompleted();
@@ -405,13 +410,13 @@ public void call(Integer i) {
405410

406411
@Override
407412
public void call() {
408-
System.out.println("-------- Done Emitting from Source ---------");
413+
// System.out.println("-------- Done Emitting from Source ---------");
409414
}
410-
}).observeOn(scheduler).doOnNext(new Action1<Integer>() {
415+
}).observeOn(scheduler, bufferSize).doOnNext(new Action1<Integer>() {
411416

412417
@Override
413418
public void call(Integer i) {
414-
System.out.println(">> onNext to slowConsumer [" + Thread.currentThread() + "] pre-take: " + i);
419+
// System.out.println(">> onNext to slowConsumer [" + Thread.currentThread() + "] pre-take: " + i);
415420
//force it to be slower than the producer
416421
try {
417422
Thread.sleep(10);
@@ -420,15 +425,22 @@ public void call(Integer i) {
420425
}
421426
countTaken.incrementAndGet();
422427
}
423-
}).take(10).toBlockingObservable().last();
428+
}).take(10).doOnNext(new Action1<Integer>() {
429+
430+
@Override
431+
public void call(Integer t) {
432+
System.out.println("*********** value: " + t);
433+
}
434+
435+
}).toBlockingObservable().last();
424436

425437
if (scheduler instanceof TrampolineScheduler || scheduler instanceof ImmediateScheduler || scheduler instanceof TestScheduler) {
426438
// since there is no concurrency it will block and only emit as many as it can process
427439
assertEquals(10, countEmitted.get());
428440
} else {
429441
// the others with concurrency should not emit all 100 ... but 10 + 2 in the pipeline
430442
// NOTE: The +2 could change if the implementation of the queue logic changes. See Javadoc at top of class.
431-
assertEquals(12, countEmitted.get());
443+
assertEquals(11, countEmitted.get(), bufferSize); // can be up to 11 + bufferSize
432444
}
433445
// number received after take (but take will filter any extra)
434446
assertEquals(10, value);

0 commit comments

Comments
 (0)