RxJS version 2.3.25
This release was mainly focused on performance as we continue to push RxJS forward.
In this release, there were the following changes:
- Performance Upgrades
- Method Fusion
Observable.prototype.pluck
Changes- Fixes
Performance Upgrades
RxJS has been committed to bringing the best performance possible. Lately we have been making changes to make it even faster while keeping the memory footprint low. To that end, we've been making strides little by little to identify bottlenecks and eliminate them.
We've been doing the following to ensure performance:
- Add benchmarks versus previous versions
- Optimize scheduler usage
- Reducing scope chains
- Method Fusion
- Enhance for Engine Optimizations
Add Benchmarks
To measure our progress, we have set up benchmark tests for performance in the tests/perf/operators
folder using benchmark.js which compares the previous version of RxJS to the current edge version. This will also give you insight into what changes we are making and how much of a difference it is making.
To give a concrete example, we were able to make the following improvements to operators such as Observable.prototype.map
:
Using node.js v0.10.36 on a Surface 2 Pro as your mileage may vary based upon your machine.
λ node map.js
old x 5,471 ops/sec ±5.04% (82 runs sampled)
new x 7,529 ops/sec ±4.61% (87 runs sampled)
Fastest is new
As you'll notice, the operations per second increase is rather dramatic in some cases, especially for map
and filter
where we reduced method scopes and created classes for both an Observable
and Observer
.
Optimize Scheduler Usage
Another optimization we made is to use schedulers more efficiently, especially around using scheduleWithState
, and scheduleRecursiveWithState
which are more efficient than their non-state counterparts of schedule
and scheduleRecursive
. One such example is how Observable.fromArray
was optimized. Previously, we had an index outside the scope of the scheduler which was incremented, and chained scopes can be expensive if you mutate variables. In this case, we are no longer doing that and instead, start with a state of 0 and then incrementing it via the recursive call to self(i + 1)
.
Observable.fromArray = function (array, scheduler) {
var len = array.length;
isScheduler(scheduler) || (scheduler = currentThreadScheduler);
return new AnonymousObservable(function (observer) {
return scheduler.scheduleRecursiveWithState(0, function (i, self) {
if (i < len) {
observer.onNext(array[i]);
self(i + 1);
} else {
observer.onCompleted();
}
});
});
};
And we're not stopping here to make schedulers more efficient since they are the heart of Rx.
Reducing Scope Chains
If you looked at previous implementations of map
, you'll notice there are nested scope chains, especially with mutation of the count
variable.
Observable.prototype.map = function (selector, thisArg) {
var selectorFn = isFunction(selector) ? bindCallback(selector, thisArg, 3) : function () { return selector; },
source = this;
return new AnonymousObservable(function (o) {
var count = 0;
return source.subscribe(function (value) {
try {
var result = selectorFn(value, count++, source);
} catch (e) {
return o.onError(e);
}
o.onNext(result);
}, function (e) { o.onError(e); }, function () { o.onCompleted(); });
}, source);
};
To get around this we created classes for map
with an Observable
and Observer
here. This allows us also future optimizations such as method fusion which we will get into later.
Method Fusion
Another way we could squeeze more performance through the technique of method fusion, meaning the ability to fuse two calls into one. For example, if you have two filter
calls in a row, we could easily flatten that into a single filter
call, and the same applies to map
as well.
Observable.prototype.map = function (selector, thisArg) {
var selectorFn = isFunction(selector) ? selector : function () { return selector; };
return this instanceof MapObservable ?
this.internalMap(selector, thisArg) :
new MapObservable(this, selectorFn, thisArg);
};
Here we are detecting if there are two map
calls in a row, and then can flatten them into a single call:
MapObservable.prototype.internalMap = function (selector, thisArg) {
var self = this;
return new MapObservable(
this.source,
function (x, i, o) { return selector(self.selector(x, i, o), i, o); }, thisArg)
};
Enhance For Engine Optimizations
One bit more involved is to ensure that we are optimizing for the ways that JavaScript engines work. For example, the Bluebird library team has a good document on optimization killers for V8.
One simple example was to ensure that we were not leaking arguments anywhere. To fix that is simple, simply a call such as the following where instead of calling slice
on arguments
which can be expensive, we can simply inline the arguments
into a new Array
with the proper size.
Observable.of = function() {
var a = new Array(arguments.length), len = a.length;
for(var i = 0; i < len; i++) { a.push(arguments[i]); }
observableOf(null, a);
};
We are looking into the next version of fixing all functions with try/catch
and try/finally
to ensure that we have faster speeds which will come in the next release.
Observable.prototype.pluck
Changes
One request that we've had for some time is having the ability to get nested properties using the Observable.prototype.pluck
operator. To that and, we have added the ability to get to nested properties by giving an argument list of properties to pluck. So, for example, we could pull out the event.target.value
from the event
argument.
var element = document.querySelector('#input');
var source = Rx.Observable.fromEvent(element, 'keyup')
.pluck('target', 'value');
This is equivalent to using map
in this way:
var element = document.querySelector('#input');
var source = Rx.Observable.fromEvent(element, 'keyup')
.map(function(event) { return event.target.value; });
Why didn't we just allow a single string to pluck the values like some other libraries have done? That's because there's no easy way to break them up, after all, having a dot in the name of a variable is acceptable, thus leading to unpredictable behavior.
var foo = {
'.bar': {
'.baz': 42
}
};
If you try to use a string like target.value
it will not work in that case.
Many thanks to @sergi for his help in creating this operator.
Fixes
- Issue #495 - Fix links - @38elements
- Issue #496 - Fix documentation - @38elements
- Issue #497 - fixed typo - @g00fy-
- Issue #500 - fix link in observable.md - @38elements
- Issue #502 - fix sample code - @38elements
- Issue #505 - Fix trailing comma in
intro.js
- @mattpodwysocki - Issue #506 - fix links - @38elements
- Issue #507 - documentation fixes - @38elements
- Issue #509 - Rename
contains
toincludes
- Issue #510 - Fix valueOf for hash calls - @wasphub
- Issue #511 - Documentation fixes - @38elements
- Issue #513 - Regression in
AnonymousSubject.prototype.subscribe
- @mattpodwysocki - Issue #514 - Fixing documentation links - @zebulonj
- Issue #516 - Minor sample code fix - @zebulonj
- Issue #517 - Fix example in document - @38elements
- Issue #518 - Fix callbacks.md - @varunmc
- Issue #520 - Fix scheduler.md - @38elements
- Issue #525 - Documentation fixes - @38elements
- Issue #526 - Implement
deepPluck
- @sergi - Issue #530 - Fix parentheses in
spawn
- @cultofmetatron - Issue #534 - Fix html - @rutsky
- Issue #535 -
subscribe
ignoresthisArg
- @mattpodwysocki - Issue #578 - Fixing small issue in docs - @unao