Skip to content
This repository was archived by the owner on Oct 19, 2018. It is now read-only.

Rewriting in JS

Mitch VanDuyn edited this page Aug 4, 2017 · 6 revisions

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.

Other Observations

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.

Clone this wiki locally