package io.nextop.rx; import android.util.Log; import android.view.View; import immutablecollections.ImSet; import rx.Notification; import rx.Observable; import rx.Subscriber; import rx.functions.Action0; import rx.functions.Action2; import rx.subjects.BehaviorSubject; import rx.subscriptions.Subscriptions; import javax.annotation.Nullable; import java.util.*; // must be called on the main thread public final class RxDebugger { private static final RxDebugger global = new RxDebugger(); public static RxDebugger get() { return global; } static final String TAG = "RxDebugger"; private final Map<Subscriber<?>, Stats> statsMap; private ImSet<Subscriber<? super Stats>> subscribers; /** non-null when stepping */ @Nullable private Queue<Step> stepping = null; private final BehaviorSubject stepStatePublish; private long headSubscriberId = 0; private boolean suppressed = false; RxDebugger() { statsMap = new LinkedHashMap<Subscriber<?>, Stats>(8); subscribers = ImSet.empty(); stepStatePublish = BehaviorSubject.create(new StepState(false, 0)); } public boolean isEnabled() { return !suppressed; } void update(final Stats stats) { if (suppressed) { assert false : "Callers must check isEnabled."; return; } if (!stats.active) { throw new IllegalArgumentException(); } @Nullable Stats r = statsMap.put(stats.subscriber, stats); if (null != r) { r.active = false; stats.subscriberId = r.subscriberId; } else { stats.subscriberId = headSubscriberId++; } stats.nanos = System.nanoTime(); // do this to avoid an infinite recursion where dispatching to the debugger observers // triggers more debug stats suppressed = true; try { for (Subscriber<? super Stats> subscriber : subscribers) { // check if the debug subscribers triggered a newer update on the stats subscriber // if so, do not publish old stats // this should probably trigger a warning (debug observer has side effects) if (!stats.active) { break; } try { subscriber.onNext(stats); } catch (Throwable t) { // FIXME severe Log.e(TAG, null, t); } } } finally { suppressed = false; } if (stats.active && stats.removed) { stats.active = false; statsMap.remove(stats.subscriber); } } void deliver(Subscriber subscriber, Notification notification, Action2<Subscriber, Notification> action) { if (null != stepping) { stepping.add(new Step(subscriber, notification, action)); publishStepState(); } else { action.call(subscriber, notification); } } // STEPPING public boolean step() { if (null != stepping) { @Nullable Step step = stepping.poll(); if (null != step) { step.action.call(step.subscriber, step.notification); publishStepState(); return true; } } return false; } public void setStepping(boolean s) { if (s) { if (null == stepping) { stepping = new LinkedList<Step>(); publishStepState(); } } else { if (null != stepping) { for (@Nullable Step step; null != (step = stepping.poll()); ) { step.action.call(step.subscriber, step.notification); } assert stepping.isEmpty(); stepping = null; publishStepState(); } } } private void publishStepState() { stepStatePublish.onNext(createStepState()); } private StepState createStepState() { if (null != stepping) { return new StepState(true, stepping.size()); } else { return new StepState(false, 0); } } // INTROSPECTION public Observable<StepState> getStepState() { return stepStatePublish; } /** * a stream of stats, one object per subscriber. * Newer stats replace older stats for the same subscriber. */ public Observable<Stats> getStats() { return Observable.create(new Observable.OnSubscribe<Stats>() { @Override public void call(final Subscriber<? super Stats> subscriber) { subscriber.add(Subscriptions.create(new Action0() { @Override public void call() { subscribers = subscribers.removing(subscriber); } })); subscribers = subscribers.adding(subscriber); for (Stats stats : new ArrayList<Stats>(statsMap.values())) { // this should probably trigger a warning (debug observer has side effects) if (stats.active) { try { subscriber.onNext(stats); } catch (Throwable t) { // FIXME severe Log.e(TAG, null, t); } } } } }); } public static final class Stats { public static final int F_NEXT = 0x01; public static final int F_COMPLETED = 0x02; public static final int F_ERROR = 0x04; public static final int F_FAILED = 0x08; public static final int F_CONNECTED = 0x10; public static final int F_DISCONNECTED = 0x20; public final int flags; public final Subscriber<?> subscriber; @Nullable public final View view; public final boolean removed; public final boolean connected; public final int onNextCount; public final int onCompletedCount; public final int onErrorCount; @Nullable public final Notification mostRecentNotification; // FAILED NOTIFICATIONS // this is where a call to oNext, onCompleted, onError failed public final int failedNotificationCount; @Nullable public final Notification mostRecentFailedNotification; @Nullable public final Throwable mostRecentFailedNotificationReason; // internally updated public long subscriberId; public long nanos; // internal boolean active = true; Stats(int flags, Subscriber<?> subscriber, @Nullable View view, boolean removed, boolean connected, int onNextCount, int onCompletedCount, int onErrorCount, @Nullable Notification mostRecentNotification, int failedNotificationCount, @Nullable Notification mostRecentFailedNotification, @Nullable Throwable mostRecentFailedNotificationReason) { this.flags = flags; this.subscriber = subscriber; this.view = view; this.removed = removed; this.connected = connected; this.onNextCount = onNextCount; this.onCompletedCount = onCompletedCount; this.onErrorCount = onErrorCount; this.mostRecentNotification = mostRecentNotification; this.failedNotificationCount = failedNotificationCount; this.mostRecentFailedNotification = mostRecentFailedNotification; this.mostRecentFailedNotificationReason = mostRecentFailedNotificationReason; } } public static final class StepState { public final boolean stepping; public final int stepCount; StepState(boolean stepping, int stepCount) { this.stepping = stepping; this.stepCount = stepCount; } } // internal private static final class Step { Subscriber subscriber; Notification notification; Action2<Subscriber, Notification> action; Step(Subscriber subscriber, Notification notification, Action2<Subscriber, Notification> action) { this.subscriber = subscriber; this.notification = notification; this.action = action; } } }