From bbd55b152c340e460879722748330e1e35bcc073 Mon Sep 17 00:00:00 2001 From: Nick Fellows Date: Tue, 19 Jun 2018 17:23:52 -0500 Subject: [PATCH] some refactoring to improve testability / minimize the use of globals --- README.md | 58 +----- build.gradle | 2 +- docs/performance.md | 52 +++++ .../com/halfhp/rxtracer/AssemblyWrapper.java | 41 ++++ .../rxtracer/CompletableObserverWrapper.java | 27 +++ .../rxtracer/FlowableSubscriberWrapper.java | 32 ++++ .../halfhp/rxtracer/MaybeObserverWrapper.java | 32 ++++ .../main/java/com/halfhp/rxtracer/Mode.java | 26 +++ .../com/halfhp/rxtracer/ObserverWrapper.java | 32 ++++ .../java/com/halfhp/rxtracer/RxTracer.java | 180 +++++++++--------- .../rxtracer/SingleObserverWrapper.java | 27 +++ .../halfhp/rxtracer/StackTraceRewriter.java | 42 ++++ .../com/halfhp/rxtracer/TracingObserver.java | 6 +- .../java/io/reactivex/TracingCompletable.java | 32 +--- .../java/io/reactivex/TracingFlowable.java | 37 +--- .../main/java/io/reactivex/TracingMaybe.java | 37 +--- .../java/io/reactivex/TracingObservable.java | 37 +--- .../main/java/io/reactivex/TracingSingle.java | 32 +--- .../CompletableObserverWrapperTest.java | 27 +++ .../com/halfhp/rxtracer/ExampleService.java | 33 ---- .../FlowableSubscriberWrapperTest.java | 21 ++ .../rxtracer/MaybeObserverWrapperTest.java | 20 ++ .../halfhp/rxtracer/ObserverWrapperTest.java | 18 ++ .../halfhp/rxtracer/RxTracerBenchmark.java | 87 ++++++++- .../com/halfhp/rxtracer/RxTracerTest.java | 153 +++------------ .../rxtracer/SingleObserverWrapperTest.java | 19 ++ .../rxtracer/StackTraceRewriterTest.java | 34 ++++ .../java/com/halfhp/rxtracer/WrapperTest.java | 15 ++ .../halfhp/rxtracer/test/ExampleService.java | 69 +++++++ .../halfhp/rxtracer/test/TraceChecker.java | 37 ++++ 30 files changed, 807 insertions(+), 458 deletions(-) create mode 100644 docs/performance.md create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/AssemblyWrapper.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/CompletableObserverWrapper.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/FlowableSubscriberWrapper.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/MaybeObserverWrapper.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/Mode.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/ObserverWrapper.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/SingleObserverWrapper.java create mode 100644 rxtracer/src/main/java/com/halfhp/rxtracer/StackTraceRewriter.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/CompletableObserverWrapperTest.java delete mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/ExampleService.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/FlowableSubscriberWrapperTest.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/MaybeObserverWrapperTest.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/ObserverWrapperTest.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/SingleObserverWrapperTest.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/StackTraceRewriterTest.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/WrapperTest.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/test/ExampleService.java create mode 100644 rxtracer/src/test/java/com/halfhp/rxtracer/test/TraceChecker.java diff --git a/README.md b/README.md index 021a459..8bfe47b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # RxTracer [![Codix](https://codix.io/gh/badge/halfhp/rxtracer)](https://codix.io/gh/repo/halfhp/rxtracer) [![CircleCI](https://circleci.com/gh/halfhp/rxtracer.svg?style=shield)](https://circleci.com/gh/halfhp/rxtracer) -A Utility to rewrite RxJava2 stack traces to include the original subscribe call-site. +A [faster](docs/performance.md) way to rewrite RxJava2 stack traces to include the original subscribe call-site. ## Usage @@ -56,57 +56,7 @@ exceptions that rx adds such as an `UndeliverableException` if the exception occ has no error handler. ## Is It Slow? - -### The short Answer: - -If the performance overhead of using RxJava in your project didn't scare you away then the overhead of -RxTracer shouldn't scare you either. - -### The Long Answer: - -While capturing a stack trace is a relatively slow operation, a trace is captured only once per subscription and -subscription tends to be an infrequent operation: You create an observable, you subscribe to it and you operate -on it's stream of emissions. There are many emissions, but only one subscription*. - -_\* It has been pointed out that operations such as `flatMap` result in additional subscriptions. This is true but -point about the ratio between subscriptions and emissions still holds. I've added a benchmark below to -capture this scenario._ - -Use cases do exist where hundreds or more subscriptions are firing every second. If -your project falls into that category then you'll want measure the performance impact of using rxtracer. -For most software projects, particularly mobile apps, desktop apps and and apps not running on a server, -the overhead is typically negligible. - -These sorts of assertions are notoriously difficult to prove one way or another, but I'll attempt to -use a Caliper microbenchmark to provide some baseline numbers: - -For those interested, the source of the microbenchmark is [available here](rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java). - -I ran the following benchmarks on a 2016 Macbook Pro with a 2.9ghz Intel i7 CPU. - -The first benchmark, `measureInstantiateOnly`, measures only the time taken to instantiate a new Observable. -Most of the heavy lifting is done at this stage so these results paint RxTracer in -the worst possible light. - -* **DISABLED:** 7.8ns per instantiation -* **ENABLED:** 14.2ns per instantiation - -You'll almost never instantiate an observable without subscribing to it though, since that would be pointless. As a -real-world example the second benchmark `measureInstantiatePlusSubscibe` measures the combined time of instantiation and subscription: - -* **DISABLED:** 53ns per instantiate-subscribe -* **ENABLED:** 62ns per instantiate-subscribe - -Practically nothing is happening in the body of subscribe and we're still only -looking at a overhead of about 17%. In most real-world scenarios we're very likely looking at less than 10%. - -The final benchmark `measureLongFlatMapSubscribeChain` instantiates an observable, runs it through five successive -invocations of `flatMap` and then subscribes to the result. - -* **DISABLED** 295.38μs per instantiate-flatmap-5x-susbscribe -* **ENABLED** 313.75μs per instantiate-flatmap-5x-susbscribe - -While both benchmarks shoot up into the microseconds range (possibly due to me misusing Caliper), the measured overhead -of RxTracer is roughly 6%. Five successive `flatMap` invocations might be a tad high to be representative of an average case, -but keep in mind that each invocation is doing the bare minimum of work. +If the overhead of using RxJava in your project didn't scare you away then the overhead of +RxTracer shouldn't scare you either. For a longer answer completely with some benchmarks +check out the [performance notes](docs/performance.md). diff --git a/build.gradle b/build.gradle index 59a7a33..6b474f5 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { } ext { - theVersionName = '0.1.2' + theVersionName = '0.1.3' } allprojects { diff --git a/docs/performance.md b/docs/performance.md new file mode 100644 index 0000000..cbb0f0c --- /dev/null +++ b/docs/performance.md @@ -0,0 +1,52 @@ +# Performance +RxTracer uses preemptive stack traces to provide call-site details when an exception occurs. + +While capturing a stack trace is a relatively slow operation, a trace is captured only once per subscription and +subscription tends to be an infrequent operation: You create an observable, you subscribe to it and you operate +on it's stream of emissions. There are many emissions, but only one subscription*. + +_\* It has been pointed out that operations such as `flatMap` result in additional subscriptions. This is true but +point about the ratio between subscriptions and emissions still holds. I've added a benchmark below to +capture this scenario._ + +Use cases do exist where hundreds or more subscriptions are firing every second. If +your project falls into that category then you'll want measure the performance impact of using rxtracer. +For most software projects, particularly mobile apps, desktop apps and and apps not running on a server, +the overhead is typically negligible. + +These sorts of assertions are notoriously difficult to prove one way or another, but I'll attempt to +use a Caliper microbenchmark to provide some baseline numbers: + +For those interested, the source of the microbenchmark is [available here](rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java). + +I ran the following benchmarks on a 2016 Macbook Pro with a 2.9ghz Intel i7 CPU. + +The first benchmark, `measureInstantiateOnly`, measures only the time taken to instantiate a new Observable. +Most of the heavy lifting is done at this stage so these results paint RxTracer in +the worst possible light. + +* **DISABLED:** 7.8ns per instantiation +* **ENABLED:** 14.2ns per instantiation + +You'll almost never instantiate an observable without subscribing to it though, since that would be pointless. As a +real-world example the second benchmark `measureInstantiatePlusSubscibe` measures the combined time of instantiation and subscription: + +* **DISABLED:** 53ns per instantiate-subscribe +* **ENABLED:** 62ns per instantiate-subscribe + +Practically nothing is happening in the body of subscribe and we're still only +looking at a overhead of about 17%. In most real-world scenarios we're very likely looking at less than 10%. + +The final benchmark `measureLongFlatMapSubscribeChain` instantiates an observable, runs it through five successive +invocations of `flatMap` and then subscribes to the result. + +* **DISABLED** 295.38μs per instantiate-flatmap-5x-susbscribe +* **ENABLED** 313.75μs per instantiate-flatmap-5x-susbscribe + +While both benchmarks shoot up into the microseconds range (possibly due to me misusing Caliper), the measured overhead +of RxTracer is roughly 6%. Five successive `flatMap` invocations might be a tad high to be representative of an average case, +but keep in mind that each invocation is doing the bare minimum of work. + +## RxTracer vs Traceur +The [benchmarks](rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java) include a Traceur profile for each of the above tests. +While the benchmarks show RxTracer is about 15% faster than Traceur on average. \ No newline at end of file diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/AssemblyWrapper.java b/rxtracer/src/main/java/com/halfhp/rxtracer/AssemblyWrapper.java new file mode 100644 index 0000000..28d2530 --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/AssemblyWrapper.java @@ -0,0 +1,41 @@ +package com.halfhp.rxtracer; + +import io.reactivex.annotations.NonNull; +import io.reactivex.annotations.Nullable; +import io.reactivex.functions.Function; + +/** + * Wraps an existing rx assembly so that it can be invoked after rxtracer's own assembly. + * @param + */ +class AssemblyWrapper implements Function { + private final Function wrappedAssembly; + private final Function tracer; + + private AssemblyWrapper( + @NonNull Function tracer, + @Nullable Function wrappedAssembly) { + this.tracer = tracer; + this.wrappedAssembly = wrappedAssembly; + } + + @Nullable + Function getWrappedAssembly() { + return wrappedAssembly; + } + + @Override + public T apply(T t) throws Exception { + if (wrappedAssembly != null) { + return wrappedAssembly.apply(tracer.apply(t)); + } else { + return tracer.apply(t); + } + } + + static Function wrap( + @NonNull Function tracer, + @Nullable Function wrappedAssembly) { + return new AssemblyWrapper<>(tracer, wrappedAssembly); + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/CompletableObserverWrapper.java b/rxtracer/src/main/java/com/halfhp/rxtracer/CompletableObserverWrapper.java new file mode 100644 index 0000000..1348024 --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/CompletableObserverWrapper.java @@ -0,0 +1,27 @@ +package com.halfhp.rxtracer; + +import io.reactivex.CompletableObserver; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; + +public class CompletableObserverWrapper extends TracingObserver implements CompletableObserver { + + public CompletableObserverWrapper(@NonNull CompletableObserver wrapped, @NonNull StackTraceRewriter rewriter) { + super(wrapped, rewriter); + } + + @Override + public void onSubscribe(Disposable d) { + wrapped.onSubscribe(d); + } + + @Override + public void onComplete() { + wrapped.onComplete(); + } + + @Override + public void onError(Throwable t) { + wrapped.onError(rewriter.rewrite(t, this.stackTrace)); + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/FlowableSubscriberWrapper.java b/rxtracer/src/main/java/com/halfhp/rxtracer/FlowableSubscriberWrapper.java new file mode 100644 index 0000000..662aba4 --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/FlowableSubscriberWrapper.java @@ -0,0 +1,32 @@ +package com.halfhp.rxtracer; + +import io.reactivex.annotations.NonNull; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +public class FlowableSubscriberWrapper extends TracingObserver> implements Subscriber { + + public FlowableSubscriberWrapper(@NonNull Subscriber wrapped, @NonNull StackTraceRewriter rewriter) { + super(wrapped, rewriter); + } + + @Override + public void onSubscribe(Subscription s) { + wrapped.onSubscribe(s); + } + + @Override + public void onNext(T t) { + wrapped.onNext(t); + } + + @Override + public void onError(Throwable e) { + wrapped.onError(rewriter.rewrite(e, this.stackTrace)); + } + + @Override + public void onComplete() { + wrapped.onComplete(); + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/MaybeObserverWrapper.java b/rxtracer/src/main/java/com/halfhp/rxtracer/MaybeObserverWrapper.java new file mode 100644 index 0000000..810f3db --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/MaybeObserverWrapper.java @@ -0,0 +1,32 @@ +package com.halfhp.rxtracer; + +import io.reactivex.MaybeObserver; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; + +public class MaybeObserverWrapper extends TracingObserver> implements MaybeObserver { + + public MaybeObserverWrapper(@NonNull MaybeObserver wrapped, @NonNull StackTraceRewriter rewriter) { + super(wrapped, rewriter); + } + + @Override + public void onSubscribe(Disposable d) { + wrapped.onSubscribe(d); + } + + @Override + public void onSuccess(T t) { + wrapped.onSuccess(t); + } + + @Override + public void onError(Throwable e) { + wrapped.onError(rewriter.rewrite(e, this.stackTrace)); + } + + @Override + public void onComplete() { + wrapped.onComplete(); + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/Mode.java b/rxtracer/src/main/java/com/halfhp/rxtracer/Mode.java new file mode 100644 index 0000000..7074bd6 --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/Mode.java @@ -0,0 +1,26 @@ +package com.halfhp.rxtracer; + +/** + * + */ +public enum Mode { + /** + * The default mode. Completely replaces the stack trace of the initial exception thrown + * from within a subscribe callback with the stack trace that was recorded when subscribe was originally invoked. + * This is typically the most appropriate and expected approach. + */ + REWRITE, + + /** + * Append the the stack trace recorded when subscribe was invoked to the stack trace of the + * exception generated when the subscribe callback ran + */ + APPEND, + + /** + * Experimental - prepend the the stack trace recorded when subscribe was invoked to the stack trace of the + * exception generated when the subscribe callback ran. This approach can sometimes produce + * better stack trace aggregation in bug trackers like Crashalytics etc. + */ + PREPEND +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/ObserverWrapper.java b/rxtracer/src/main/java/com/halfhp/rxtracer/ObserverWrapper.java new file mode 100644 index 0000000..ba510c3 --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/ObserverWrapper.java @@ -0,0 +1,32 @@ +package com.halfhp.rxtracer; + +import io.reactivex.Observer; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; + +public class ObserverWrapper extends TracingObserver> implements Observer { + + public ObserverWrapper(@NonNull Observer wrapped, @NonNull StackTraceRewriter rewriter) { + super(wrapped, rewriter); + } + + @Override + public void onSubscribe(Disposable d) { + wrapped.onSubscribe(d); + } + + @Override + public void onNext(T t) { + wrapped.onNext(t); + } + + @Override + public void onError(Throwable t) { + wrapped.onError(rewriter.rewrite(t, this.stackTrace)); + } + + @Override + public void onComplete() { + wrapped.onComplete(); + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/RxTracer.java b/rxtracer/src/main/java/com/halfhp/rxtracer/RxTracer.java index 2881fb2..5d980da 100644 --- a/rxtracer/src/main/java/com/halfhp/rxtracer/RxTracer.java +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/RxTracer.java @@ -11,129 +11,135 @@ import io.reactivex.TracingObservable; import io.reactivex.TracingSingle; import io.reactivex.annotations.NonNull; -import io.reactivex.annotations.Nullable; import io.reactivex.functions.Function; import io.reactivex.plugins.RxJavaPlugins; +import static io.reactivex.plugins.RxJavaPlugins.getOnCompletableAssembly; + public class RxTracer { - private static Mode mode = Mode.REWRITE; + private StackTraceRewriter rewriter = new StackTraceRewriter(Mode.REWRITE); + private static RxTracer instance = new RxTracer(); + + public static void setMode(@NonNull Mode mode) { + instance.getRewriter().setMode(mode); + } + + public static RxTracer getInstance() { + return RxTracer.instance; + } /** - * + * May be used to provide a customized tracer implementation. Take care to set your custom instance + * BEFORE invoking {@link #enable()}. + * @param instance */ - public enum Mode { - /** - * The default mode. Completely replaces the stack trace of the initial exception thrown - * from within a subscribe callback with the stack trace that was recorded when subscribe was originally invoked. - * This is typically the most appropriate and expected approach. - */ - REWRITE, - - /** - * Append the the stack trace recorded when subscribe was invoked to the stack trace of the - * exception generated when the subscribe callback ran - */ - APPEND, - - /** - * Experimental - prepend the the stack trace recorded when subscribe was invoked to the stack trace of the - * exception generated when the subscribe callback ran. This approach can sometimes produce - * better stack trace aggregation in bug trackers like Crashalytics etc. - */ - PREPEND + public static void setInstance(@NonNull RxTracer instance) { + RxTracer.instance = instance; } - public static void setMode(@NonNull Mode mode) { - RxTracer.mode = mode; + /** + * Enables tracing, preventing redundant assembly wrappers if already enabled. + */ + public static void enable() { + if(!(RxJavaPlugins.getOnCompletableAssembly() instanceof AssemblyWrapper)) { + RxJavaPlugins.setOnCompletableAssembly(instance.newOnCompletableAssembly()); + } + + if(!(RxJavaPlugins.getOnObservableAssembly() instanceof AssemblyWrapper)) { + RxJavaPlugins.setOnObservableAssembly(instance.newOnObservableAssembly()); + } + + if(!(RxJavaPlugins.getOnSingleAssembly() instanceof AssemblyWrapper)) { + RxJavaPlugins.setOnSingleAssembly(instance.newOnSingleAssembly()); + } + + if(!(RxJavaPlugins.getOnMaybeAssembly() instanceof AssemblyWrapper)) { + RxJavaPlugins.setOnMaybeAssembly(instance.newOnMaybeAssembly()); + } + + if(!(RxJavaPlugins.getOnFlowableAssembly() instanceof AssemblyWrapper)) { + RxJavaPlugins.setOnFlowableAssembly(instance.newOnFlowableAssembly()); + } } - public static void enable() { - RxJavaPlugins.setOnCompletableAssembly(wrapAssembly(new Function() { + protected Function newOnCompletableAssembly() { + return AssemblyWrapper.wrap(new Function() { @Override public Completable apply(Completable wrapped) { - return new TracingCompletable(wrapped); + return new TracingCompletable(wrapped, instance.getRewriter()); } - }, RxJavaPlugins.getOnCompletableAssembly())); - + }, getOnCompletableAssembly()); + } - RxJavaPlugins.setOnObservableAssembly(wrapAssembly(new Function() { + protected Function newOnObservableAssembly() { + return AssemblyWrapper.wrap(new Function() { @Override public Observable apply(Observable wrapped) { - return new TracingObservable(wrapped); + return new TracingObservable(wrapped, instance.getRewriter()); } - }, RxJavaPlugins.getOnObservableAssembly())); - + }, RxJavaPlugins.getOnObservableAssembly()); + } - RxJavaPlugins.setOnSingleAssembly(wrapAssembly(new Function() { + protected Function newOnSingleAssembly() { + return AssemblyWrapper.wrap(new Function() { @Override public Single apply(Single wrapped) { - return new TracingSingle(wrapped); + return new TracingSingle(wrapped, instance.getRewriter()); } - }, RxJavaPlugins.getOnSingleAssembly())); - + }, RxJavaPlugins.getOnSingleAssembly()); + } - RxJavaPlugins.setOnMaybeAssembly(wrapAssembly(new Function() { + protected Function newOnMaybeAssembly() { + return AssemblyWrapper.wrap(new Function() { @Override public Maybe apply(Maybe wrapped) { - return new TracingMaybe(wrapped); + return new TracingMaybe(wrapped, instance.getRewriter()); } - }, RxJavaPlugins.getOnMaybeAssembly())); + }, RxJavaPlugins.getOnMaybeAssembly()); + } - RxJavaPlugins.setOnFlowableAssembly(wrapAssembly(new Function() { + protected Function newOnFlowableAssembly() { + return AssemblyWrapper.wrap(new Function() { @Override public Flowable apply(Flowable wrapped) { - return new TracingFlowable(wrapped); + return new TracingFlowable(wrapped, instance.getRewriter()); } - }, RxJavaPlugins.getOnFlowableAssembly())); - } - - public static void disable() { - throw new UnsupportedOperationException("Not yet implemented"); - } - - public static T rewriteStackTrace(@NonNull T throwable, StackTraceElement[] elements) { - switch(mode) { - case APPEND: - throwable.setStackTrace(concat(throwable.getStackTrace(), elements)); - break; - case PREPEND: - throwable.setStackTrace(concat(elements, throwable.getStackTrace())); - break; - case REWRITE: - throwable.setStackTrace(elements); - break; - } - return throwable; + }, RxJavaPlugins.getOnFlowableAssembly()); } - private static StackTraceElement[] concat(StackTraceElement[] first, StackTraceElement[] second) { - final StackTraceElement[] concatenatedTrace = new StackTraceElement[first.length + second.length]; - System.arraycopy(first, 0, concatenatedTrace, 0, first.length); - System.arraycopy(second, 0, concatenatedTrace, first.length, second.length); - return concatenatedTrace; + protected StackTraceRewriter getRewriter() { + return this.rewriter; } /** - * Wraps a preexisting assembly if present, so that it it can remain in the assembly pipeline - * along with rxtracer. - * @param tracer - * @param originalAssembly - * @param - * @return + * Disables RxTracer, restoring previously installed assemblies, if any. */ - private static Function wrapAssembly( - @NonNull final Function tracer, - @Nullable final Function originalAssembly) { - return new Function() { - @Override - public T apply(T t) throws Exception { - if (originalAssembly != null) { - return originalAssembly.apply(tracer.apply(t)); - } else { - return tracer.apply(t); - } - } - }; + public static void disable() { + final Function completableWrapper = RxJavaPlugins.getOnCompletableAssembly(); + RxJavaPlugins.setOnCompletableAssembly( + completableWrapper instanceof AssemblyWrapper ? + ((AssemblyWrapper) completableWrapper).getWrappedAssembly() : null); + + final Function observableWrapper = RxJavaPlugins.getOnObservableAssembly(); + RxJavaPlugins.setOnObservableAssembly( + observableWrapper instanceof AssemblyWrapper ? + ((AssemblyWrapper) observableWrapper).getWrappedAssembly() : null); + + final Function singleWrapper = RxJavaPlugins.getOnSingleAssembly(); + RxJavaPlugins.setOnSingleAssembly( + singleWrapper instanceof AssemblyWrapper ? + ((AssemblyWrapper) singleWrapper).getWrappedAssembly() : null); + + final Function maybeWrapper = RxJavaPlugins.getOnMaybeAssembly(); + RxJavaPlugins.setOnMaybeAssembly( + maybeWrapper instanceof AssemblyWrapper ? + ((AssemblyWrapper) maybeWrapper).getWrappedAssembly() : null); + + final Function flowableWrapper = RxJavaPlugins.getOnFlowableAssembly(); + RxJavaPlugins.setOnFlowableAssembly( + flowableWrapper instanceof AssemblyWrapper ? + ((AssemblyWrapper) flowableWrapper).getWrappedAssembly() : null); } + } diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/SingleObserverWrapper.java b/rxtracer/src/main/java/com/halfhp/rxtracer/SingleObserverWrapper.java new file mode 100644 index 0000000..a9af10e --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/SingleObserverWrapper.java @@ -0,0 +1,27 @@ +package com.halfhp.rxtracer; + +import io.reactivex.SingleObserver; +import io.reactivex.annotations.NonNull; +import io.reactivex.disposables.Disposable; + +public class SingleObserverWrapper extends TracingObserver> implements SingleObserver { + + public SingleObserverWrapper(@NonNull SingleObserver wrapped, @NonNull StackTraceRewriter rewriter) { + super(wrapped, rewriter); + } + + @Override + public void onSubscribe(Disposable d) { + wrapped.onSubscribe(d); + } + + @Override + public void onSuccess(T t) { + wrapped.onSuccess(t); + } + + @Override + public void onError(Throwable e) { + wrapped.onError(rewriter.rewrite(e, this.stackTrace)); + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/StackTraceRewriter.java b/rxtracer/src/main/java/com/halfhp/rxtracer/StackTraceRewriter.java new file mode 100644 index 0000000..14dd6e1 --- /dev/null +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/StackTraceRewriter.java @@ -0,0 +1,42 @@ +package com.halfhp.rxtracer; + +import io.reactivex.annotations.NonNull; + +public class StackTraceRewriter { + + private Mode mode; + + public StackTraceRewriter(@NonNull Mode mode) { + this.mode = mode; + } + + public T rewrite(@NonNull T throwable, StackTraceElement[] elements) { + switch (mode) { + case APPEND: + throwable.setStackTrace(concat(throwable.getStackTrace(), elements)); + break; + case PREPEND: + throwable.setStackTrace(concat(elements, throwable.getStackTrace())); + break; + case REWRITE: + throwable.setStackTrace(elements); + break; + } + return throwable; + } + + public Mode getMode() { + return this.mode; + } + + public void setMode(@NonNull Mode mode) { + this.mode = mode; + } + + private static StackTraceElement[] concat(StackTraceElement[] first, StackTraceElement[] second) { + final StackTraceElement[] concatenatedTrace = new StackTraceElement[first.length + second.length]; + System.arraycopy(first, 0, concatenatedTrace, 0, first.length); + System.arraycopy(second, 0, concatenatedTrace, first.length, second.length); + return concatenatedTrace; + } +} diff --git a/rxtracer/src/main/java/com/halfhp/rxtracer/TracingObserver.java b/rxtracer/src/main/java/com/halfhp/rxtracer/TracingObserver.java index 1ddfb69..90ea425 100644 --- a/rxtracer/src/main/java/com/halfhp/rxtracer/TracingObserver.java +++ b/rxtracer/src/main/java/com/halfhp/rxtracer/TracingObserver.java @@ -1,12 +1,16 @@ package com.halfhp.rxtracer; +import io.reactivex.annotations.NonNull; + public abstract class TracingObserver { protected final T wrapped; + protected final StackTraceRewriter rewriter; // Throwable.getStackTrace is thought to be faster than Thread.currentThread().getStackTrace(): protected final StackTraceElement[] stackTrace = new Throwable().getStackTrace(); - public TracingObserver(T wrapped) { + public TracingObserver(@NonNull T wrapped, @NonNull StackTraceRewriter rewriter) { this.wrapped = wrapped; + this.rewriter = rewriter; } } diff --git a/rxtracer/src/main/java/io/reactivex/TracingCompletable.java b/rxtracer/src/main/java/io/reactivex/TracingCompletable.java index b091d07..00bd802 100644 --- a/rxtracer/src/main/java/io/reactivex/TracingCompletable.java +++ b/rxtracer/src/main/java/io/reactivex/TracingCompletable.java @@ -1,43 +1,23 @@ package io.reactivex; -import com.halfhp.rxtracer.RxTracer; -import com.halfhp.rxtracer.TracingObserver; +import com.halfhp.rxtracer.CompletableObserverWrapper; +import com.halfhp.rxtracer.StackTraceRewriter; import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; public class TracingCompletable extends Completable { private final Completable wrapped; + private final StackTraceRewriter rewriter; - public TracingCompletable(@NonNull Completable wrapped) { + public TracingCompletable(@NonNull Completable wrapped, @NonNull StackTraceRewriter rewriter) { this.wrapped = wrapped; + this.rewriter = rewriter; } @Override protected void subscribeActual(CompletableObserver co) { - wrapped.subscribeActual(new CompletableObserverWrapper(co)); + wrapped.subscribeActual(new CompletableObserverWrapper(co, rewriter)); } - private static final class CompletableObserverWrapper extends TracingObserver implements CompletableObserver { - - CompletableObserverWrapper(@NonNull CompletableObserver wrapped) { - super((wrapped)); - } - - @Override - public void onSubscribe(Disposable d) { - wrapped.onSubscribe(d); - } - - @Override - public void onComplete() { - wrapped.onComplete(); - } - - @Override - public void onError(Throwable t) { - wrapped.onError(RxTracer.rewriteStackTrace(t, this.stackTrace)); - } - } } diff --git a/rxtracer/src/main/java/io/reactivex/TracingFlowable.java b/rxtracer/src/main/java/io/reactivex/TracingFlowable.java index 0cc70be..ea7eb40 100644 --- a/rxtracer/src/main/java/io/reactivex/TracingFlowable.java +++ b/rxtracer/src/main/java/io/reactivex/TracingFlowable.java @@ -1,50 +1,25 @@ package io.reactivex; -import com.halfhp.rxtracer.RxTracer; -import com.halfhp.rxtracer.TracingObserver; +import com.halfhp.rxtracer.FlowableSubscriberWrapper; +import com.halfhp.rxtracer.StackTraceRewriter; import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import io.reactivex.annotations.NonNull; public class TracingFlowable extends Flowable { private final Flowable wrapped; + private final StackTraceRewriter rewriter; - public TracingFlowable(@NonNull Flowable wrapped) { + public TracingFlowable(@NonNull Flowable wrapped, @NonNull StackTraceRewriter rewriter) { this.wrapped = wrapped; + this.rewriter = rewriter; } @Override protected void subscribeActual(Subscriber observer) { - wrapped.subscribeActual(new FlowableSubscriberWrapper<>(observer)); + wrapped.subscribeActual(new FlowableSubscriberWrapper<>(observer, rewriter)); } - private static final class FlowableSubscriberWrapper extends TracingObserver> implements Subscriber { - - FlowableSubscriberWrapper(@NonNull Subscriber wrapped) { - super((wrapped)); - } - - @Override - public void onSubscribe(Subscription s) { - wrapped.onSubscribe(s); - } - - @Override - public void onNext(T t) { - wrapped.onNext(t); - } - - @Override - public void onError(Throwable e) { - wrapped.onError(RxTracer.rewriteStackTrace(e, this.stackTrace)); - } - - @Override - public void onComplete() { - wrapped.onComplete(); - } - } } diff --git a/rxtracer/src/main/java/io/reactivex/TracingMaybe.java b/rxtracer/src/main/java/io/reactivex/TracingMaybe.java index 7561379..073ba43 100644 --- a/rxtracer/src/main/java/io/reactivex/TracingMaybe.java +++ b/rxtracer/src/main/java/io/reactivex/TracingMaybe.java @@ -1,48 +1,23 @@ package io.reactivex; -import com.halfhp.rxtracer.RxTracer; -import com.halfhp.rxtracer.TracingObserver; +import com.halfhp.rxtracer.MaybeObserverWrapper; +import com.halfhp.rxtracer.StackTraceRewriter; import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; public class TracingMaybe extends Maybe { private final Maybe wrapped; + private final StackTraceRewriter rewriter; - public TracingMaybe(@NonNull Maybe wrapped) { + public TracingMaybe(@NonNull Maybe wrapped, @NonNull StackTraceRewriter rewriter) { this.wrapped = wrapped; + this.rewriter = rewriter; } @Override protected void subscribeActual(MaybeObserver observer) { - wrapped.subscribeActual(new MaybeObserverWrapper<>(observer)); + wrapped.subscribeActual(new MaybeObserverWrapper<>(observer, rewriter)); } - private static final class MaybeObserverWrapper extends TracingObserver> implements MaybeObserver { - - MaybeObserverWrapper(@NonNull MaybeObserver wrapped) { - super((wrapped)); - } - - @Override - public void onSubscribe(Disposable d) { - wrapped.onSubscribe(d); - } - - @Override - public void onSuccess(T t) { - wrapped.onSuccess(t); - } - - @Override - public void onError(Throwable e) { - wrapped.onError(RxTracer.rewriteStackTrace(e, this.stackTrace)); - } - - @Override - public void onComplete() { - wrapped.onComplete(); - } - } } diff --git a/rxtracer/src/main/java/io/reactivex/TracingObservable.java b/rxtracer/src/main/java/io/reactivex/TracingObservable.java index 06b78cc..ba0161f 100644 --- a/rxtracer/src/main/java/io/reactivex/TracingObservable.java +++ b/rxtracer/src/main/java/io/reactivex/TracingObservable.java @@ -1,48 +1,23 @@ package io.reactivex; -import com.halfhp.rxtracer.RxTracer; -import com.halfhp.rxtracer.TracingObserver; +import com.halfhp.rxtracer.ObserverWrapper; +import com.halfhp.rxtracer.StackTraceRewriter; import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; public class TracingObservable extends Observable { private final Observable wrapped; + private final StackTraceRewriter rewriter; - public TracingObservable(@NonNull Observable wrapped) { + public TracingObservable(@NonNull Observable wrapped, @NonNull StackTraceRewriter rewriter) { this.wrapped = wrapped; + this.rewriter = rewriter; } @Override protected void subscribeActual(io.reactivex.Observer observer) { - wrapped.subscribeActual(new ObserverWrapper<>(observer)); + wrapped.subscribeActual(new ObserverWrapper<>(observer, rewriter)); } - private static final class ObserverWrapper extends TracingObserver> implements io.reactivex.Observer { - - ObserverWrapper(@NonNull Observer wrapped) { - super((wrapped)); - } - - @Override - public void onSubscribe(Disposable d) { - wrapped.onSubscribe(d); - } - - @Override - public void onNext(T t) { - wrapped.onNext(t); - } - - @Override - public void onError(Throwable t) { - wrapped.onError(RxTracer.rewriteStackTrace(t, this.stackTrace)); - } - - @Override - public void onComplete() { - wrapped.onComplete(); - } - } } diff --git a/rxtracer/src/main/java/io/reactivex/TracingSingle.java b/rxtracer/src/main/java/io/reactivex/TracingSingle.java index 4026f5d..194d7a3 100644 --- a/rxtracer/src/main/java/io/reactivex/TracingSingle.java +++ b/rxtracer/src/main/java/io/reactivex/TracingSingle.java @@ -1,43 +1,23 @@ package io.reactivex; -import com.halfhp.rxtracer.RxTracer; -import com.halfhp.rxtracer.TracingObserver; +import com.halfhp.rxtracer.SingleObserverWrapper; +import com.halfhp.rxtracer.StackTraceRewriter; import io.reactivex.annotations.NonNull; -import io.reactivex.disposables.Disposable; public class TracingSingle extends Single { private final Single wrapped; + private final StackTraceRewriter rewriter; - public TracingSingle(@NonNull Single wrapped) { + public TracingSingle(@NonNull Single wrapped, @NonNull StackTraceRewriter rewriter) { this.wrapped = wrapped; + this.rewriter = rewriter; } @Override protected void subscribeActual(SingleObserver observer) { - wrapped.subscribeActual(new SingleObserverWrapper<>(observer)); + wrapped.subscribeActual(new SingleObserverWrapper<>(observer, rewriter)); } - private static final class SingleObserverWrapper extends TracingObserver> implements SingleObserver { - - SingleObserverWrapper(@NonNull SingleObserver wrapped) { - super((wrapped)); - } - - @Override - public void onSubscribe(Disposable d) { - wrapped.onSubscribe(d); - } - - @Override - public void onSuccess(T t) { - wrapped.onSuccess(t); - } - - @Override - public void onError(Throwable e) { - wrapped.onError(RxTracer.rewriteStackTrace(e, this.stackTrace)); - } - } } diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/CompletableObserverWrapperTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/CompletableObserverWrapperTest.java new file mode 100644 index 0000000..f7523a4 --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/CompletableObserverWrapperTest.java @@ -0,0 +1,27 @@ +package com.halfhp.rxtracer; + +import com.halfhp.rxtracer.test.ExampleService; +import com.halfhp.rxtracer.test.TraceChecker; + +import io.reactivex.Completable; +import io.reactivex.CompletableSource; +import io.reactivex.functions.Function; +import org.junit.Test; + +public class CompletableObserverWrapperTest extends WrapperTest { + + @Test + public void completable_runtimeException_hasSubscribeInStackTrace() throws Exception { + final TraceChecker traceChecker = new TraceChecker(CompletableObserverWrapperTest.class, 23); + exampleService.getFooObservable() + .flatMapCompletable(new Function() { + @Override + public CompletableSource apply(ExampleService.Foo foo) { + return Completable.complete(); + } + }) + .subscribe(new ExampleService.FailOnSuccessAction(), new ExampleService.CheckTraceOnErrorConsumer(traceChecker)); + Thread.sleep(100); + traceChecker.assertValid(); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/ExampleService.java b/rxtracer/src/test/java/com/halfhp/rxtracer/ExampleService.java deleted file mode 100644 index 84cd0fa..0000000 --- a/rxtracer/src/test/java/com/halfhp/rxtracer/ExampleService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.halfhp.rxtracer; - -import org.junit.Ignore; - -import io.reactivex.Observable; -import io.reactivex.schedulers.Schedulers; - -import java.util.concurrent.Callable; - -@Ignore -public class ExampleService { - - public Observable getFooObservable() { - return Observable.fromCallable(new Callable() { - @Override - public Foo call() { - throw new RuntimeException("Bla!"); - } - }).subscribeOn(Schedulers.newThread()); - } - - public Observable getBarObservable() { - return Observable.fromCallable(new Callable() { - @Override - public Bar call() { - return new Bar(); - } - }).subscribeOn(Schedulers.newThread()); - } - - static class Foo {} - static class Bar {} -} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/FlowableSubscriberWrapperTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/FlowableSubscriberWrapperTest.java new file mode 100644 index 0000000..6ee91a1 --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/FlowableSubscriberWrapperTest.java @@ -0,0 +1,21 @@ +package com.halfhp.rxtracer; + +import com.halfhp.rxtracer.test.ExampleService; +import com.halfhp.rxtracer.test.TraceChecker; + +import io.reactivex.BackpressureStrategy; +import org.junit.Test; + +public class FlowableSubscriberWrapperTest extends WrapperTest { + + + @Test + public void flowable_runtimeException_hasSubscribeInStackTrace() throws Exception { + final TraceChecker traceChecker = new TraceChecker(FlowableSubscriberWrapperTest.class, 17); + exampleService.getFooObservable() + .toFlowable(BackpressureStrategy.DROP) + .subscribe(new ExampleService.FailOnSuccessConsumer(), new ExampleService.CheckTraceOnErrorConsumer(traceChecker)); + Thread.sleep(100); + traceChecker.assertValid(); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/MaybeObserverWrapperTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/MaybeObserverWrapperTest.java new file mode 100644 index 0000000..aafe6ef --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/MaybeObserverWrapperTest.java @@ -0,0 +1,20 @@ +package com.halfhp.rxtracer; + +import com.halfhp.rxtracer.test.ExampleService; +import com.halfhp.rxtracer.test.TraceChecker; + +import org.junit.Test; + +public class MaybeObserverWrapperTest extends WrapperTest { + + + @Test + public void maybe_runtimeException_hasSubscribeInStackTrace() throws Exception { + final TraceChecker traceChecker = new TraceChecker(MaybeObserverWrapperTest.class, 16); + exampleService.getFooObservable() + .singleElement() + .subscribe(new ExampleService.FailOnSuccessConsumer(), new ExampleService.CheckTraceOnErrorConsumer(traceChecker)); + Thread.sleep(100); + traceChecker.assertValid(); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/ObserverWrapperTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/ObserverWrapperTest.java new file mode 100644 index 0000000..ddf12e2 --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/ObserverWrapperTest.java @@ -0,0 +1,18 @@ +package com.halfhp.rxtracer; + +import com.halfhp.rxtracer.test.ExampleService; +import com.halfhp.rxtracer.test.TraceChecker; + +import org.junit.Test; + +public class ObserverWrapperTest extends WrapperTest { + + @Test + public void observable_runtimeException_hasSubscribeInStackTrace() throws Exception { + final TraceChecker traceChecker = new TraceChecker(ObserverWrapperTest.class, 14); + exampleService.getFooObservable() + .subscribe(new ExampleService.FailOnSuccessConsumer(), new ExampleService.CheckTraceOnErrorConsumer(traceChecker)); + Thread.sleep(100); + traceChecker.assertValid(); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java b/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java index 8537db9..4086800 100644 --- a/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerBenchmark.java @@ -3,6 +3,8 @@ import com.google.caliper.Benchmark; import com.google.caliper.api.VmOptions; import com.google.caliper.runner.CaliperMain; +import com.halfhp.rxtracer.test.ExampleService; +import com.tspoon.traceur.Traceur; import io.reactivex.ObservableSource; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; @@ -14,25 +16,55 @@ public class RxTracerBenchmark { private final ExampleService exampleService = new ExampleService(); - { - //Traceur.enableLogging(); // use this to benchmark against Traceur - RxTracer.enable(); // comment this out to get DISABLED stats - } - /** * Used to temporarily store op results to prevent overly aggressive optimizations */ Object optimizerPrevention; - @Benchmark - void measureInstantiateOnly(int reps) { + private void preInit(boolean isRxTracerEnabled, boolean isTraceurEnabled) { + if(isRxTracerEnabled) { + Traceur.disableLogging(); + RxTracer.enable(); + } else if(isTraceurEnabled) { + RxTracer.disable(); + Traceur.enableLogging(); + } else { + RxTracer.disable(); + Traceur.disableLogging(); + } + } + + // INSTANTIATE_ONLY BENCHMARKS + + private void instantiateOnlyLoop(int reps) { for(int i = 0; i < reps; i++) { optimizerPrevention = exampleService.getBarObservable(); } } @Benchmark - void measureInstantiatePlusSubscribe(int reps) { + void instantiateOnly_baseline(int reps) { + preInit(false, false); + instantiateOnlyLoop(reps); + } + + @Benchmark + void instantiateOnly_rxtracer(int reps) { + preInit(true, false); + instantiateOnlyLoop(reps); + } + + @Benchmark + void instantiateOnly_traceur(int reps) { + preInit(false, true); + instantiateOnlyLoop(reps); + } + + + + // SUBSCRIBE BENCHMARKS + + private void subscribeLoop(int reps) { for(int i = 0; i < reps; i++) { exampleService.getBarObservable().subscribe(new Consumer() { @Override @@ -44,7 +76,26 @@ public void accept(ExampleService.Bar foo) { } @Benchmark - void measureLongFlatMapSubscribeChain(int reps) { + void subscribe_baseline(int reps) { + preInit(false, false); + subscribeLoop(reps); + } + + @Benchmark + void subscribe_rxtracer(int reps) { + preInit(true, false); + subscribeLoop(reps); + } + + @Benchmark + void subscribe_traceur(int reps) { + preInit(false, true); + subscribeLoop(reps); + } + + // MULTI-FLATMAP BENCHMARKS + + private void multiFlatMapLoop(int reps) { for(int i = 0; i < reps; i++) { exampleService.getBarObservable() .flatMap(new Function>() { @@ -90,6 +141,24 @@ public void accept(ExampleService.Bar foo) { } } + @Benchmark + void multiFlatMap_baseline(int reps) { + preInit(false, false); + multiFlatMapLoop(reps); + } + + @Benchmark + void multiFlatMap_rxtracer(int reps) { + preInit(true, false); + multiFlatMapLoop(reps); + } + + @Benchmark + void multiFlatMap_traceur(int reps) { + preInit(false, true); + multiFlatMapLoop(reps); + } + public static void main(String[] args) { CaliperMain.main(RxTracerBenchmark.class, args); } diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerTest.java index 2f0b484..bb484e3 100644 --- a/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerTest.java +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/RxTracerTest.java @@ -1,23 +1,17 @@ package com.halfhp.rxtracer; +import io.reactivex.plugins.RxJavaPlugins; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; -import io.reactivex.Completable; -import io.reactivex.CompletableSource; -import io.reactivex.annotations.NonNull; -import io.reactivex.functions.Action; -import io.reactivex.functions.Consumer; -import io.reactivex.functions.Function; - -import static junit.framework.Assert.fail; +import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class RxTracerTest { - private ExampleService exampleService = new ExampleService(); - @BeforeClass public static void beforeClass() { RxTracer.enable(); @@ -25,135 +19,38 @@ public static void beforeClass() { @Before public void before() { - RxTracer.setMode(RxTracer.Mode.REWRITE); - } - - @Test - public void completable_runtimeException_hasSubscribeInStackTrace() throws Exception { - final TraceChecker traceChecker = new TraceChecker(); - exampleService.getFooObservable() - .flatMapCompletable(new Function() { - @Override - public CompletableSource apply(ExampleService.Foo foo) { - return Completable.complete(); - } - }) - .subscribe(new Action() { - @Override - public void run() { // expect this line in the trace - } - }, new Consumer() { - @Override - public void accept(Throwable e) { - traceChecker.check(e, 41); - } - }); - Thread.sleep(100); - traceChecker.assertValid(); + RxTracer.setMode(Mode.REWRITE); } @Test - public void observable_runtimeException_hasSubscribeInStackTrace() throws Exception { - final TraceChecker traceChecker = new TraceChecker(); - exampleService.getFooObservable() - .subscribe(new Consumer() { - @Override - public void accept(ExampleService.Foo foo) { // expect this line in the trace - } - }, new Consumer() { - @Override - public void accept(Throwable e) { - traceChecker.check(e, 59); - } - }); - Thread.sleep(100); - traceChecker.assertValid(); + public void enable_enablesRxTracer() { + assertTrue(RxJavaPlugins.getOnCompletableAssembly() instanceof AssemblyWrapper); + assertTrue(RxJavaPlugins.getOnObservableAssembly() instanceof AssemblyWrapper); + assertTrue(RxJavaPlugins.getOnSingleAssembly() instanceof AssemblyWrapper); + assertTrue(RxJavaPlugins.getOnMaybeAssembly() instanceof AssemblyWrapper); + assertTrue(RxJavaPlugins.getOnFlowableAssembly() instanceof AssemblyWrapper); } @Test - public void single_runtimeException_hasSubscribeInStackTrace() throws Exception { - final TraceChecker traceChecker = new TraceChecker(); - exampleService.getFooObservable() - .singleOrError() - .subscribe(new Consumer() { - @Override - public void accept(ExampleService.Foo foo) { // expect this line in the trace - } - }, new Consumer() { - @Override - public void accept(Throwable e) { - traceChecker.check(e, 78); - } - }); - Thread.sleep(100); - traceChecker.assertValid(); + public void enable_isIdempotent() { + Object startingAssembly = RxJavaPlugins.getOnCompletableAssembly(); + RxTracer.enable(); + assertEquals(startingAssembly, RxJavaPlugins.getOnCompletableAssembly()); } @Test - public void maybe_runtimeException_hasSubscribeInStackTrace() throws Exception { - final TraceChecker traceChecker = new TraceChecker(); - exampleService.getFooObservable() - .singleElement() - .subscribe(new Consumer() { - @Override - public void accept(ExampleService.Foo foo) { // expect this line in the trace - } - }, new Consumer() { - @Override - public void accept(Throwable e) { - traceChecker.check(e, 97); - } - }); - Thread.sleep(100); - traceChecker.assertValid(); + public void disable_disablesRxTracer() { + RxTracer.disable(); + assertNull(RxJavaPlugins.getOnCompletableAssembly()); + assertNull(RxJavaPlugins.getOnObservableAssembly()); + assertNull(RxJavaPlugins.getOnSingleAssembly()); + assertNull(RxJavaPlugins.getOnMaybeAssembly()); + assertNull(RxJavaPlugins.getOnFlowableAssembly()); } + @Ignore @Test - public void rewriteStacktrace_append_addsNewTraceToEnd() { - RxTracer.setMode(RxTracer.Mode.APPEND); - final StackTraceElement[] newTrace = new Exception().getStackTrace(); - final Exception ex1 = new Exception(); - final StackTraceElement[] rawTrace = ex1.getStackTrace(); - - assertEquals(rawTrace.length + newTrace.length, RxTracer.rewriteStackTrace(ex1, newTrace).getStackTrace().length); - - // first element in the original trace should remain first: - assertEquals(rawTrace[0], ex1.getStackTrace()[0]); - } - - @Test - public void rewriteStacktrace_prepend_addsTraeToStart() { - RxTracer.setMode(RxTracer.Mode.PREPEND); - final StackTraceElement[] newTrace = new Exception().getStackTrace(); - final Exception ex1 = new Exception(); - final StackTraceElement[] rawTrace = ex1.getStackTrace(); - - assertEquals(rawTrace.length + newTrace.length, RxTracer.rewriteStackTrace(ex1, newTrace).getStackTrace().length); - - // first element in the new trace should now be first: - assertEquals(newTrace[0], ex1.getStackTrace()[0]); - } - - - private static class TraceChecker { - private String errorMessage = null; - - - void check(@NonNull Throwable e, @NonNull int lineNumber) { - e.printStackTrace(); - final String className = RxTracerTest.class.getName(); - for (StackTraceElement element : e.getStackTrace()) { - if (element.getClassName().equals(className) && element.getLineNumber() == lineNumber) { - return; - } - } - this.errorMessage = "Trace missing an entry for " + className + ":" + lineNumber; - } - - void assertValid() { - if (errorMessage != null) { - fail(errorMessage); - } - } + public void disable_restoresWrappedAssembly() { + // TODO } } \ No newline at end of file diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/SingleObserverWrapperTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/SingleObserverWrapperTest.java new file mode 100644 index 0000000..1156cfa --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/SingleObserverWrapperTest.java @@ -0,0 +1,19 @@ +package com.halfhp.rxtracer; + +import com.halfhp.rxtracer.test.ExampleService; +import com.halfhp.rxtracer.test.TraceChecker; + +import org.junit.Test; + +public class SingleObserverWrapperTest extends WrapperTest { + + @Test + public void single_runtimeException_hasSubscribeInStackTrace() throws Exception { + final TraceChecker traceChecker = new TraceChecker(SingleObserverWrapperTest.class, 15); + exampleService.getFooObservable() + .singleOrError() + .subscribe(new ExampleService.FailOnSuccessConsumer(), new ExampleService.CheckTraceOnErrorConsumer(traceChecker)); + Thread.sleep(100); + traceChecker.assertValid(); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/StackTraceRewriterTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/StackTraceRewriterTest.java new file mode 100644 index 0000000..39966c9 --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/StackTraceRewriterTest.java @@ -0,0 +1,34 @@ +package com.halfhp.rxtracer; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class StackTraceRewriterTest { + + @Test + public void rewriteStacktrace_append_addsNewTraceToEnd() { + final StackTraceRewriter rewriter = new StackTraceRewriter(Mode.APPEND); + final StackTraceElement[] newTrace = new Exception().getStackTrace(); + final Exception ex1 = new Exception(); + final StackTraceElement[] rawTrace = ex1.getStackTrace(); + + assertEquals(rawTrace.length + newTrace.length, rewriter.rewrite(ex1, newTrace).getStackTrace().length); + + // first element in the original trace should remain first: + assertEquals(rawTrace[0], ex1.getStackTrace()[0]); + } + + @Test + public void rewriteStacktrace_prepend_addsTraeToStart() { + final StackTraceRewriter rewriter = new StackTraceRewriter(Mode.PREPEND); + final StackTraceElement[] newTrace = new Exception().getStackTrace(); + final Exception ex1 = new Exception(); + final StackTraceElement[] rawTrace = ex1.getStackTrace(); + + assertEquals(rawTrace.length + newTrace.length, rewriter.rewrite(ex1, newTrace).getStackTrace().length); + + // first element in the new trace should now be first: + assertEquals(newTrace[0], ex1.getStackTrace()[0]); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/WrapperTest.java b/rxtracer/src/test/java/com/halfhp/rxtracer/WrapperTest.java new file mode 100644 index 0000000..2341036 --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/WrapperTest.java @@ -0,0 +1,15 @@ +package com.halfhp.rxtracer; + +import com.halfhp.rxtracer.test.ExampleService; + +import org.junit.BeforeClass; + +public abstract class WrapperTest { + + protected ExampleService exampleService = new ExampleService(); + + @BeforeClass + public static void beforeClass() { + RxTracer.enable(); + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/test/ExampleService.java b/rxtracer/src/test/java/com/halfhp/rxtracer/test/ExampleService.java new file mode 100644 index 0000000..b47b70d --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/test/ExampleService.java @@ -0,0 +1,69 @@ +package com.halfhp.rxtracer.test; + +import org.junit.Ignore; + +import io.reactivex.Observable; +import io.reactivex.annotations.NonNull; +import io.reactivex.functions.Action; +import io.reactivex.functions.Consumer; +import io.reactivex.schedulers.Schedulers; + +import java.util.concurrent.Callable; + +import static org.junit.Assert.fail; + +@Ignore +public class ExampleService { + + public Observable getFooObservable() { + return Observable.fromCallable(new Callable() { + @Override + public Foo call() { + throw new RuntimeException("Bla!"); + } + }).subscribeOn(Schedulers.newThread()); + } + + public Observable getBarObservable() { + return Observable.fromCallable(new Callable() { + @Override + public Bar call() { + return new Bar(); + } + }).subscribeOn(Schedulers.newThread()); + } + + public static class Foo {} + public static class Bar {} + + public static class FailOnSuccessConsumer implements Consumer { + + @Override + public void accept(Foo foo) throws Exception { + fail("Exception expected"); + } + } + + public static class FailOnSuccessAction implements Action { + + @Override + public void run() throws Exception { + fail("Exception expected"); + } + } + + public static class CheckTraceOnErrorConsumer implements Consumer { + + @NonNull + private TraceChecker traceChecker; + + public CheckTraceOnErrorConsumer(@NonNull TraceChecker traceChecker) { + this.traceChecker = traceChecker; + } + + @Override + public void accept(Throwable throwable) throws Exception { + traceChecker.check(throwable); + } + } +} diff --git a/rxtracer/src/test/java/com/halfhp/rxtracer/test/TraceChecker.java b/rxtracer/src/test/java/com/halfhp/rxtracer/test/TraceChecker.java new file mode 100644 index 0000000..02bcf21 --- /dev/null +++ b/rxtracer/src/test/java/com/halfhp/rxtracer/test/TraceChecker.java @@ -0,0 +1,37 @@ +package com.halfhp.rxtracer.test; + +import io.reactivex.annotations.NonNull; +import org.junit.Ignore; + +import static junit.framework.Assert.fail; + +@Ignore +public class TraceChecker { + private String errorMessage = null; + + @NonNull + private final String expectedClassName; + private final int expectedLineNumber; + + public TraceChecker(@NonNull Class expectedClass, int expectedLineNumber) { + this.expectedClassName = expectedClass.getName(); + this.expectedLineNumber = expectedLineNumber; + } + + public void check(@NonNull Throwable e) { + e.printStackTrace(); + for (StackTraceElement element : e.getStackTrace()) { + final String thisClassName = element.getClassName(); + if (thisClassName.equals(expectedClassName) && element.getLineNumber() == expectedLineNumber) { + return; + } + } + this.errorMessage = "Trace missing an entry for " + expectedClassName + ":" + expectedLineNumber; + } + + public void assertValid() { + if (errorMessage != null) { + fail(errorMessage); + } + } +}