-
Notifications
You must be signed in to change notification settings - Fork 18
Rewriting in JS
What would happen if the entire hyperloop internal code base was rewritten in JS and used JS classes and objects, instead of in opal-ruby?
Here is an experiment:
Take the following code (from hyper-model)
module React
class State
ALWAYS_UPDATE_STATE_AFTER_RENDER = Hyperloop.on_client? # if on server then we don't wait to update the state
@rendering_level = 0
class << self
attr_reader :current_observer
def observers_by_name
@observers_by_name ||= Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
end
def states
@states ||= Hash.new { |h, k| h[k] = {} }
end
def has_observers?(object, name)
!observers_by_name[object][name].empty?
end
def bulk_update
saved_bulk_update_flag = @bulk_update_flag
@bulk_update_flag = true
yield
ensure
@bulk_update_flag = saved_bulk_update_flag
end
def set_state2(object, name, value, updates, exclusions = nil)
# set object's name state to value, tell all observers it has changed.
# Observers must implement update_react_js_state
object_needs_notification = object.respond_to? :update_react_js_state
observers_by_name[object][name].dup.each do |observer|
next if exclusions && exclusions.include?(observer)
updates[observer] += [object, name, value]
object_needs_notification = false if object == observer
end
updates[object] += [nil, name, value] if object_needs_notification
end
def initialize_states(object, initial_values) # initialize objects' name/value pairs
states[object].merge!(initial_values || {})
end
def get_state(object, name, current_observer = @current_observer)
# get current value of name for object, remember that the current object depends on this state,
# current observer can be overriden with last param
if current_observer && !new_observers[current_observer][object].include?(name)
new_observers[current_observer][object] << name
end
if @delayed_updates && @delayed_updates[object][name]
@delayed_updates[object][name][1] << current_observer
end
states[object][name]
end
# lots of other methods ... follow
end
end
end
That is 46 lines of code excluding comments and blank lines.
Opal compiles this to the following JS code:
/* Generated by Opal 0.10.3 */
(function(Opal) {
function $rb_plus(lhs, rhs) {
return (typeof(lhs) === 'number' && typeof(rhs) === 'number') ? lhs + rhs : lhs['$+'](rhs);
}
var self = Opal.top, $scope = Opal, nil = Opal.nil, $breaker = Opal.breaker, $slice = Opal.slice, $module = Opal.module, $klass = Opal.klass, $hash2 = Opal.hash2;
Opal.add_stubs(['$on_client?', '$attr_reader', '$new', '$[]=', '$!', '$empty?', '$[]', '$observers_by_name', '$respond_to?', '$each', '$include?', '$+', '$==', '$dup', '$merge!', '$states', '$new_observers', '$<<']);
return (function($base) {
var $React, self = $React = $module($base, 'React');
var def = self.$$proto, $scope = self.$$scope;
(function($base, $super) {
function $State(){};
var self = $State = $klass($base, $super, 'State', $State);
var def = self.$$proto, $scope = self.$$scope;
Opal.cdecl($scope, 'ALWAYS_UPDATE_STATE_AFTER_RENDER', $scope.get('Hyperloop')['$on_client?']());
self.rendering_level = 0;
return (function(self) {
var $scope = self.$$scope, def = self.$$proto, TMP_3, TMP_5, TMP_6, TMP_7, TMP_9, TMP_10, TMP_11;
self.$attr_reader("current_observer");
Opal.defn(self, '$observers_by_name', TMP_3 = function $$observers_by_name() {
var $a, $b, $c, TMP_1, self = this;
if (self.observers_by_name == null) self.observers_by_name = nil;
return ((($a = self.observers_by_name) !== false && $a !== nil && $a != null) ? $a : self.observers_by_name = ($b = ($c = $scope.get('Hash')).$new, $b.$$p = (TMP_1 = function(h, k){var self = TMP_1.$$s || this, $d, $e, TMP_2;
if (h == null) h = nil;if (k == null) k = nil;
return h['$[]='](k, ($d = ($e = $scope.get('Hash')).$new, $d.$$p = (TMP_2 = function(h, k){var self = TMP_2.$$s || this;
if (h == null) h = nil;if (k == null) k = nil;
return h['$[]='](k, [])}, TMP_2.$$s = self, TMP_2.$$arity = 2, TMP_2), $d).call($e))}, TMP_1.$$s = self, TMP_1.$$arity = 2, TMP_1), $b).call($c));
}, TMP_3.$$arity = 0);
Opal.defn(self, '$states', TMP_5 = function $$states() {
var $a, $b, $c, TMP_4, self = this;
if (self.states == null) self.states = nil;
return ((($a = self.states) !== false && $a !== nil && $a != null) ? $a : self.states = ($b = ($c = $scope.get('Hash')).$new, $b.$$p = (TMP_4 = function(h, k){var self = TMP_4.$$s || this;
if (h == null) h = nil;if (k == null) k = nil;
return h['$[]='](k, $hash2([], {}))}, TMP_4.$$s = self, TMP_4.$$arity = 2, TMP_4), $b).call($c));
}, TMP_5.$$arity = 0);
Opal.defn(self, '$has_observers?', TMP_6 = function(object, name) {
var self = this;
return self.$observers_by_name()['$[]'](object)['$[]'](name)['$empty?']()['$!']();
}, TMP_6.$$arity = 2);
Opal.defn(self, '$bulk_update', TMP_7 = function $$bulk_update() {
var self = this, $iter = TMP_7.$$p, $yield = $iter || nil, saved_bulk_update_flag = nil;
if (self.bulk_update_flag == null) self.bulk_update_flag = nil;
TMP_7.$$p = null;
try {
saved_bulk_update_flag = self.bulk_update_flag;
self.bulk_update_flag = true;
return Opal.yieldX($yield, []);;
} finally {
self.bulk_update_flag = saved_bulk_update_flag
};
}, TMP_7.$$arity = 0);
Opal.defn(self, '$set_state2', TMP_9 = function $$set_state2(object, name, value, updates, exclusions) {
var $a, $b, TMP_8, $c, self = this, object_needs_notification = nil;
if (exclusions == null) {
exclusions = nil;
}
object_needs_notification = object['$respond_to?']("update_react_js_state");
($a = ($b = self.$observers_by_name()['$[]'](object)['$[]'](name).$dup()).$each, $a.$$p = (TMP_8 = function(observer){var self = TMP_8.$$s || this, $c, $d;
if (observer == null) observer = nil;
if ((($c = (($d = exclusions !== false && exclusions !== nil && exclusions != null) ? exclusions['$include?'](observer) : exclusions)) !== nil && $c != null && (!$c.$$is_boolean || $c == true))) {
return nil;};
($c = observer, $d = updates, $d['$[]=']($c, $rb_plus($d['$[]']($c), [object, name, value])));
if (object['$=='](observer)) {
return object_needs_notification = false
} else {
return nil
};}, TMP_8.$$s = self, TMP_8.$$arity = 1, TMP_8), $a).call($b);
if (object_needs_notification !== false && object_needs_notification !== nil && object_needs_notification != null) {
return ($a = object, $c = updates, $c['$[]=']($a, $rb_plus($c['$[]']($a), [nil, name, value])))
} else {
return nil
};
}, TMP_9.$$arity = -5);
Opal.defn(self, '$initialize_states', TMP_10 = function $$initialize_states(object, initial_values) {
var $a, self = this;
return self.$states()['$[]'](object)['$merge!'](((($a = initial_values) !== false && $a !== nil && $a != null) ? $a : $hash2([], {})));
}, TMP_10.$$arity = 2);
return (Opal.defn(self, '$get_state', TMP_11 = function $$get_state(object, name, current_observer) {
var $a, $b, self = this;
if (self.delayed_updates == null) self.delayed_updates = nil;
if (self.current_observer == null) self.current_observer = nil;
if (current_observer == null) {
current_observer = self.current_observer;
}
if ((($a = (($b = current_observer !== false && current_observer !== nil && current_observer != null) ? self.$new_observers()['$[]'](current_observer)['$[]'](object)['$include?'](name)['$!']() : current_observer)) !== nil && $a != null && (!$a.$$is_boolean || $a == true))) {
self.$new_observers()['$[]'](current_observer)['$[]'](object)['$<<'](name)};
if ((($a = ($b = self.delayed_updates, $b !== false && $b !== nil && $b != null ?self.delayed_updates['$[]'](object)['$[]'](name) : $b)) !== nil && $a != null && (!$a.$$is_boolean || $a == true))) {
self.delayed_updates['$[]'](object)['$[]'](name)['$[]'](1)['$<<'](current_observer)};
return self.$states()['$[]'](object)['$[]'](name);
}, TMP_11.$$arity = -3), nil) && 'get_state';
})(Opal.get_singleton_class(self));
})($scope.base, null)
})($scope.base)
})(Opal);
Which is 109 lines of pretty dense Javascript.
How about minification: http://refresh-sf.com/
Minified this will be 3,255 bytes, and then gzipping will reduce this to which minified-gzipped will be 1,190 bytes. (a total 82% minification ratio)
Here is the same functionality written in JS:
React.State = function () {
var ALWAYS_UPDATE_STATE_AFTER_RENDER = Opal.Hyperloop['$on_client?']();
var rendering_level = 0;
var _current_observer;
var by_name = {};
var _new = {};
var bulk_update_flag = false;
var states = {};
var set_observer = function (observer, k1, k2, value) {
if (!observer[k1]) { observer[k1] = new Object() }
if (!observer[k2]) { observer[k1][k2] = new Array() }
observer[k1][k2] = value
};
var get_observer = function (observer, k1, k2) {
if (!observer[k1]) { return null }
observer[k1][k2]
};
var set_state2 = function (object, name, value, updates, exclusions) {
var object_needs_notification = object['respond_to?']('update_react_js_state')
for (observer in get_observers(by_name, object, name).slice(0)) {
if (!exclusions || !exclusions.includes(object)) {
updates[observer].push([object, name, value])
if (object == observer) { object_needs_notification = false }
}
}
if (!object_needs_notification) { updates[object].push += [null, name, value] }
}
var get_state = function(object, name, curr_observer) {
if (!current_observer) { curr_observer = current_observer }
if (curr_observer) {
var names = get_observers[_new, curr_observer, object]
if (!names.includes(name)) { names.push(name) }
}
if (delayed_updates) {
var observers = delayed_updates[object][name]
if (observers) { observers[1].push(curr_observer) }
}
return (states[object] && states[object][name])
}
return {
current_observer: function () { return _current_observer; },
has_observers: function (object, name) {
var r = get_observers(by_name, object, name)
return r && r.length > 0
},
bulk_update: function (fn) {
var saved_bulk_update_flag = bulk_update_flag
bulk_update_flag = true
try {
fn();
}
finally {
bulk_update_flag = saved_bulk_update_flag
}
},
initialize_states: function (object, initial_values) {
states[object] = Object.assign({}, states[object] || {}, initial_values || {})
}
}
}();
Which is 60 lines of code - about 30% more than Ruby.
However minified this is only 334 bytes and once gzipped is only 240 bytes (89% total compression), almost 5X smaller than the same compiled and compressed Ruby code.
This is because the compiled ruby code is very dense and more than 2X bigger than the original ruby code. I would guess that if the compiled ruby code were formatted so that it was easily readable it would be at least twice as big again. The other problem is that minification compression ratio is much better for the straight javascript code.
Writing the JS code really sucked. It's not just that you have to write 30% more lines of code, but it's just the lack of any expressiveness. Guards are harder to write; expressions like foo ||= ... become complicated obscuring what the code is trying to do; the lack of a robust Hash initializer is especially difficult in this modules' case; also niceties like each
, empty?
are missing, and on and on. Frankly it was just a hot mess.
But given this great "inefficiency" of Ruby code once compiled, doesn't this hurt Hyperloop?
Not really. Hyperloop (like Rails) is providing most of the functionality, so as demonstrated by the Todo App the actual "application" code is probably 4-5 times smaller than the same functionality in JS. This means that the resulting compressed and gzipped payload is going to be just about the same. Why is the application code 4-5X smaller when the above example is only 30% smaller? Because at the application level you are accessing the rich facilities of hyperloop, while in the above example the saving is simply because of Ruby's expressiveness.
But the developer is writing code at a much higher level.