Skip to content

Commit 67b7be0

Browse files
authored
1.x: replay request coordination reduce overhead (#3470)
1 parent 08a2130 commit 67b7be0

File tree

3 files changed

+435
-109
lines changed

3 files changed

+435
-109
lines changed

src/main/java/rx/internal/operators/OperatorReplay.java

Lines changed: 166 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
import rx.*;
2323
import rx.Observable;
24-
import rx.exceptions.Exceptions;
25-
import rx.exceptions.OnErrorThrowable;
24+
import rx.exceptions.*;
2625
import rx.functions.*;
26+
import rx.internal.util.OpenHashSet;
2727
import rx.observables.ConnectableObservable;
2828
import rx.schedulers.Timestamped;
2929
import rx.subscriptions.Subscriptions;
@@ -314,8 +314,16 @@ static final class ReplaySubscriber<T> extends Subscriber<T> implements Subscrip
314314
/** Indicates a terminated ReplaySubscriber. */
315315
static final InnerProducer[] TERMINATED = new InnerProducer[0];
316316

317-
/** Tracks the subscribed producers. */
318-
final AtomicReference<InnerProducer[]> producers;
317+
/** Indicates no further InnerProducers are accepted. */
318+
volatile boolean terminated;
319+
/** Tracks the subscribed producers. Guarded by itself. */
320+
final OpenHashSet<InnerProducer<T>> producers;
321+
/** Contains a copy of the producers. Modified only from the source side. */
322+
InnerProducer<T>[] producersCache;
323+
/** Contains number of modifications to the producers set.*/
324+
volatile long producersVersion;
325+
/** Contains the number of modifications that the producersCache holds. */
326+
long producersCacheVersion;
319327
/**
320328
* Atomically changed from false to true by connect to make sure the
321329
* connection is only performed by one thread.
@@ -335,12 +343,19 @@ static final class ReplaySubscriber<T> extends Subscriber<T> implements Subscrip
335343
/** The upstream producer. */
336344
volatile Producer producer;
337345

346+
/** The queue that holds producers with request changes that need to be coordinated. */
347+
List<InnerProducer<T>> coordinationQueue;
348+
/** Indicate that all request amounts should be considered. */
349+
boolean coordinateAll;
350+
351+
@SuppressWarnings("unchecked")
338352
public ReplaySubscriber(AtomicReference<ReplaySubscriber<T>> current,
339353
ReplayBuffer<T> buffer) {
340354
this.buffer = buffer;
341355

342356
this.nl = NotificationLite.instance();
343-
this.producers = new AtomicReference<InnerProducer[]>(EMPTY);
357+
this.producers = new OpenHashSet<InnerProducer<T>>();
358+
this.producersCache = EMPTY;
344359
this.shouldConnect = new AtomicBoolean();
345360
// make sure the source doesn't produce values until the child subscribers
346361
// expressed their request amounts
@@ -351,7 +366,15 @@ void init() {
351366
add(Subscriptions.create(new Action0() {
352367
@Override
353368
public void call() {
354-
ReplaySubscriber.this.producers.getAndSet(TERMINATED);
369+
if (!terminated) {
370+
synchronized (producers) {
371+
if (!terminated) {
372+
producers.terminate();
373+
producersVersion++;
374+
terminated = true;
375+
}
376+
}
377+
}
355378
// unlike OperatorPublish, we can't null out the terminated so
356379
// late subscribers can still get replay
357380
// current.compareAndSet(ReplaySubscriber.this, null);
@@ -370,76 +393,34 @@ boolean add(InnerProducer<T> producer) {
370393
if (producer == null) {
371394
throw new NullPointerException();
372395
}
373-
// the state can change so we do a CAS loop to achieve atomicity
374-
for (;;) {
375-
// get the current producer array
376-
InnerProducer[] c = producers.get();
377-
// if this subscriber-to-source reached a terminal state by receiving
378-
// an onError or onCompleted, just refuse to add the new producer
379-
if (c == TERMINATED) {
396+
if (terminated) {
397+
return false;
398+
}
399+
synchronized (producers) {
400+
if (terminated) {
380401
return false;
381402
}
382-
// we perform a copy-on-write logic
383-
int len = c.length;
384-
InnerProducer[] u = new InnerProducer[len + 1];
385-
System.arraycopy(c, 0, u, 0, len);
386-
u[len] = producer;
387-
// try setting the producers array
388-
if (producers.compareAndSet(c, u)) {
389-
return true;
390-
}
391-
// if failed, some other operation succeeded (another add, remove or termination)
392-
// so retry
403+
404+
producers.add(producer);
405+
producersVersion++;
393406
}
407+
return true;
394408
}
395409

396410
/**
397411
* Atomically removes the given producer from the producers array.
398412
* @param producer the producer to remove
399413
*/
400414
void remove(InnerProducer<T> producer) {
401-
// the state can change so we do a CAS loop to achieve atomicity
402-
for (;;) {
403-
// let's read the current producers array
404-
InnerProducer[] c = producers.get();
405-
// if it is either empty or terminated, there is nothing to remove so we quit
406-
if (c == EMPTY || c == TERMINATED) {
407-
return;
408-
}
409-
// let's find the supplied producer in the array
410-
// although this is O(n), we don't expect too many child subscribers in general
411-
int j = -1;
412-
int len = c.length;
413-
for (int i = 0; i < len; i++) {
414-
if (c[i].equals(producer)) {
415-
j = i;
416-
break;
417-
}
418-
}
419-
// we didn't find it so just quit
420-
if (j < 0) {
421-
return;
422-
}
423-
// we do copy-on-write logic here
424-
InnerProducer[] u;
425-
// we don't create a new empty array if producer was the single inhabitant
426-
// but rather reuse an empty array
427-
if (len == 1) {
428-
u = EMPTY;
429-
} else {
430-
// otherwise, create a new array one less in size
431-
u = new InnerProducer[len - 1];
432-
// copy elements being before the given producer
433-
System.arraycopy(c, 0, u, 0, j);
434-
// copy elements being after the given producer
435-
System.arraycopy(c, j + 1, u, j, len - j - 1);
436-
}
437-
// try setting this new array as
438-
if (producers.compareAndSet(c, u)) {
415+
if (terminated) {
416+
return;
417+
}
418+
synchronized (producers) {
419+
if (terminated) {
439420
return;
440421
}
441-
// if we failed, it means something else happened
442-
// (a concurrent add/remove or termination), we need to retry
422+
producers.remove(producer);
423+
producersVersion++;
443424
}
444425
}
445426

@@ -450,7 +431,7 @@ public void setProducer(Producer p) {
450431
throw new IllegalStateException("Only a single producer can be set on a Subscriber.");
451432
}
452433
producer = p;
453-
manageRequests();
434+
manageRequests(null);
454435
replay();
455436
}
456437

@@ -493,81 +474,157 @@ public void onCompleted() {
493474
/**
494475
* Coordinates the request amounts of various child Subscribers.
495476
*/
496-
void manageRequests() {
477+
void manageRequests(InnerProducer<T> inner) {
497478
// if the upstream has completed, no more requesting is possible
498479
if (isUnsubscribed()) {
499480
return;
500481
}
501482
synchronized (this) {
502483
if (emitting) {
484+
if (inner != null) {
485+
List<InnerProducer<T>> q = coordinationQueue;
486+
if (q == null) {
487+
q = new ArrayList<InnerProducer<T>>();
488+
coordinationQueue = q;
489+
}
490+
q.add(inner);
491+
} else {
492+
coordinateAll = true;
493+
}
503494
missed = true;
504495
return;
505496
}
506497
emitting = true;
507498
}
499+
500+
long ri = maxChildRequested;
501+
long maxTotalRequested;
502+
503+
if (inner != null) {
504+
maxTotalRequested = Math.max(ri, inner.totalRequested.get());
505+
} else {
506+
maxTotalRequested = ri;
507+
508+
InnerProducer<T>[] a = copyProducers();
509+
for (InnerProducer<T> rp : a) {
510+
if (rp != null) {
511+
maxTotalRequested = Math.max(maxTotalRequested, rp.totalRequested.get());
512+
}
513+
}
514+
515+
}
516+
makeRequest(maxTotalRequested, ri);
517+
508518
for (;;) {
509519
// if the upstream has completed, no more requesting is possible
510520
if (isUnsubscribed()) {
511521
return;
512522
}
513523

514-
@SuppressWarnings("unchecked")
515-
InnerProducer<T>[] a = producers.get();
516-
517-
long ri = maxChildRequested;
518-
long maxTotalRequests = ri;
519-
520-
for (InnerProducer<T> rp : a) {
521-
maxTotalRequests = Math.max(maxTotalRequests, rp.totalRequested.get());
524+
List<InnerProducer<T>> q;
525+
boolean all;
526+
synchronized (this) {
527+
if (!missed) {
528+
emitting = false;
529+
return;
530+
}
531+
missed = false;
532+
q = coordinationQueue;
533+
coordinationQueue = null;
534+
all = coordinateAll;
535+
coordinateAll = false;
522536
}
523537

524-
long ur = maxUpstreamRequested;
525-
Producer p = producer;
538+
ri = maxChildRequested;
539+
maxTotalRequested = ri;
526540

527-
long diff = maxTotalRequests - ri;
528-
if (diff != 0) {
529-
maxChildRequested = maxTotalRequests;
530-
if (p != null) {
531-
if (ur != 0L) {
532-
maxUpstreamRequested = 0L;
533-
p.request(ur + diff);
534-
} else {
535-
p.request(diff);
536-
}
537-
} else {
538-
// collect upstream request amounts until there is a producer for them
539-
long u = ur + diff;
540-
if (u < 0) {
541-
u = Long.MAX_VALUE;
541+
if (q != null) {
542+
for (InnerProducer<T> rp : q) {
543+
maxTotalRequested = Math.max(maxTotalRequested, rp.totalRequested.get());
544+
}
545+
}
546+
547+
if (all) {
548+
InnerProducer<T>[] a = copyProducers();
549+
for (InnerProducer<T> rp : a) {
550+
if (rp != null) {
551+
maxTotalRequested = Math.max(maxTotalRequested, rp.totalRequested.get());
542552
}
543-
maxUpstreamRequested = u;
544553
}
545-
} else
546-
// if there were outstanding upstream requests and we have a producer
547-
if (ur != 0L && p != null) {
548-
maxUpstreamRequested = 0L;
549-
// fire the accumulated requests
550-
p.request(ur);
551554
}
552555

553-
synchronized (this) {
554-
if (!missed) {
555-
emitting = false;
556-
return;
556+
makeRequest(maxTotalRequested, ri);
557+
}
558+
}
559+
560+
InnerProducer<T>[] copyProducers() {
561+
synchronized (producers) {
562+
Object[] a = producers.values();
563+
int n = a.length;
564+
@SuppressWarnings("unchecked")
565+
InnerProducer<T>[] result = new InnerProducer[n];
566+
System.arraycopy(a, 0, result, 0, n);
567+
return result;
568+
}
569+
}
570+
571+
void makeRequest(long maxTotalRequests, long previousTotalRequests) {
572+
long ur = maxUpstreamRequested;
573+
Producer p = producer;
574+
575+
long diff = maxTotalRequests - previousTotalRequests;
576+
if (diff != 0) {
577+
maxChildRequested = maxTotalRequests;
578+
if (p != null) {
579+
if (ur != 0L) {
580+
maxUpstreamRequested = 0L;
581+
p.request(ur + diff);
582+
} else {
583+
p.request(diff);
557584
}
558-
missed = false;
585+
} else {
586+
// collect upstream request amounts until there is a producer for them
587+
long u = ur + diff;
588+
if (u < 0) {
589+
u = Long.MAX_VALUE;
590+
}
591+
maxUpstreamRequested = u;
559592
}
593+
} else
594+
// if there were outstanding upstream requests and we have a producer
595+
if (ur != 0L && p != null) {
596+
maxUpstreamRequested = 0L;
597+
// fire the accumulated requests
598+
p.request(ur);
560599
}
561600
}
562601

563602
/**
564603
* Tries to replay the buffer contents to all known subscribers.
565604
*/
605+
@SuppressWarnings("unchecked")
566606
void replay() {
567-
@SuppressWarnings("unchecked")
568-
InnerProducer<T>[] a = producers.get();
569-
for (InnerProducer<T> rp : a) {
570-
buffer.replay(rp);
607+
InnerProducer<T>[] pc = producersCache;
608+
if (producersCacheVersion != producersVersion) {
609+
synchronized (producers) {
610+
pc = producersCache;
611+
// if the producers hasn't changed do nothing
612+
// otherwise make a copy of the current set of producers
613+
Object[] a = producers.values();
614+
int n = a.length;
615+
if (pc.length != n) {
616+
pc = new InnerProducer[n];
617+
producersCache = pc;
618+
}
619+
System.arraycopy(a, 0, pc, 0, n);
620+
producersCacheVersion = producersVersion;
621+
}
622+
}
623+
ReplayBuffer<T> b = buffer;
624+
for (InnerProducer<T> rp : pc) {
625+
if (rp != null) {
626+
b.replay(rp);
627+
}
571628
}
572629
}
573630
}
@@ -646,7 +703,7 @@ public void request(long n) {
646703
addTotalRequested(n);
647704
// if successful, notify the parent dispatcher this child can receive more
648705
// elements
649-
parent.manageRequests();
706+
parent.manageRequests(this);
650707

651708
parent.buffer.replay(this);
652709
return;
@@ -727,7 +784,7 @@ public void unsubscribe() {
727784
// let's assume this child had 0 requested before the unsubscription while
728785
// the others had non-zero. By removing this 'blocking' child, the others
729786
// are now free to receive events
730-
parent.manageRequests();
787+
parent.manageRequests(this);
731788
}
732789
}
733790
}

0 commit comments

Comments
 (0)