Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module#prepend #1826

Merged
merged 46 commits into from
May 25, 2018
Merged

Module#prepend #1826

merged 46 commits into from
May 25, 2018

Conversation

iliabylich
Copy link
Contributor

Not ready yet.

Includes a massive rewrite of the runtime.js

  • The most important: Ancestors chain is represented via prototypes (for subclassing and modules inclusion).
  • Every Opal class has BasicObject on the bottom of the prototype chain. It makes stubs logic much, much easier. (And I think in general the logic is easier now).
  • Removed $$parent, $$included_modules, $$ancestors, $$children, $$methods.
  • There's no method donation anymore. Yes, method definition = prototype[method_name] = body + updating a list of iclasses + hooks.
  • All Objects can (and actually are) allocated via new Opal.ClassName.
  • Inheritance from String is simplified (there are only 5 or 6 failing specs, not sure if it's worth to revert it).

RubySpec suite "works for me" (for both node and chrome), going to check other tasks tomorrow.

Also this PR is called Module#prepend, so I'm also going to add Opal.prepend_features, it looks straightforward now.

Also I've just realized that we could use obj instanceof Opal.SomeClass for classes (for modules there's an iclass in the prototype chain, so it won't work). That's funny.

@iliabylich
Copy link
Contributor Author

@Mogztter Could you check please if it works with the Asciidoctor?

@iliabylich
Copy link
Contributor Author

Basically ready for reviewing.

@elia
Copy link
Member

elia commented May 15, 2018

@iliabylich super great stuff man!
I can't wait to read the diff (yes, I still have to, and yes, I had to wait until now 😅)

I didn't know about the way prepend was implemented in MRI, that's clever! Basically any Module instance (including classes) will defn on the list of protos it maintains, where:

  • for a regular class it's just [self.prototype]
  • for any module is the list of its iclasses
  • for a class with prepend it's the one iclass (having prepend_feature moving all methods from the prototype to the iclass)

is that correct?

now proceeding to the review! 😄

@iliabylich
Copy link
Contributor Author

is that correct?

Yes!


for (i = 0, length = names.length; i < length; i++) {
name = names[i];
if (name.charAt(0) === '$') {
self_singleton_class_proto[name] = other_singleton_class_proto[name];
if (name.charAt(0) === '$' && name !== '$$id') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's worth exposing Opal.is_method or using the same check?

self_singleton_class.$$const = Object.assign({}, other_singleton_class.$$const);
Object.setPrototypeOf(
self_singleton_class.prototype,
Object.getPrototypeOf(other_singleton_class.prototype)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered https://stackoverflow.com/a/23809148 ?

seems to have better performance at startup, but degrades in other cases, probably it's worth it anyway, but I'd like to be sure firefox does not become unbearably slow with proto changes

perf of rake mspec_ruby_nodejs

# prepend
11031 examples, 0 failures (time taken: 13.869999885559082)
       56.68 real        55.76 user         2.29 sys

# master
11009 examples, 0 failures (time taken: 10.92799997329712)
       54.29 real        52.99 user         2.37 sys

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's because there's too much dynamic stuff in the RubySpec suite. It create anonymous classes, extends them in runtime, I guess that's the reason. For a static system of modules it seems to be faster.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼 I need to test this on FF/mobile/… with more realistic code, just to be sure, anyway using prototypes makes everything 💯 times cleaner ✨

}

// Class doesnt exist, create a new one with given superclass...
throw new Error('Broken prototype chain');
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superclass is fixed and should be easy to cache, probably a good one after this one is done ✅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what do you mean here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean that we should consider storing a ref to the superclass right inside the class, since it can't change in ruby

// If scope is an object, use its class
if (!scope.$$is_class && !scope.$$is_module) {
scope = scope.$$class;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved inside the else of if (scope == null)

@@ -1341,7 +1216,8 @@
//
// @default [Prototype List] BasicObject_alloc.prototype
//
Opal.stub_subscribers = [BasicObject_alloc.prototype];
// FIXME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some notes on what's to fix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, we don't need stub_subscribers anymore (because all bridged classes are inherited from the BasicObject). The fix is to remove it 😄

@iliabylich
Copy link
Contributor Author

I'm getting the following results

RubySpec suite

1007 static classes, 589 static modules, 299 includes, 59 extends.
332 Class.new invocations, 134 Module.new

Chrome:

master: 11009 examples, 41 failures (time taken: 13.717999935150146)
patch: 11031 examples, 41 failures (time taken: 16.236999988555908)

FF:

master: 11009 examples, 41 failures (time taken: 24.863999843597412)
patch: 11031 examples, 41 failures (time taken: 29.003000020980835)

Safari:

master: 11009 examples, 43 failures (time taken: 19.655999898910522)
patch: 11031 examples, 43 failures (time taken: 21.611000061035156)

Boot time

78 static classes, 19 static modules, 11 includes, 2 extends.
0 Class.new invocations, 0 Module.new

Measured using

window.onload = function () {
  var loadTime = window.performance.timing.domContentLoadedEventEnd-window.performance.timing.navigationStart;
  console.log('Full load time is '+ loadTime / 1000);
}

Chrome

master: Page load time is 0.167
patch: Full load time is 0.105

FF:

master: Full load time is 0.139
patch: Full load time is 0.104

Safari:

master: Full load time is 0.343
patch: Full load time is 0.263

@iliabylich
Copy link
Contributor Author

More benchmarks on V8:

function benchmark() {
  echo $1 > test.rb
  gco master
  opal -c test.rb > test.js
  echo "Master:"
  repeat 5 time node test.js
  gco module-prepend
  echo "Patch:"
  opal -c test.rb > test.js
  repeat 5 time node test.js
}

Class.new:

$ benchmark "1000.times { Class.new }"
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Master:
  0.28s user 0.04s system 99% cpu 0.313 total
  0.27s user 0.04s system 100% cpu 0.310 total
  0.28s user 0.04s system 100% cpu 0.314 total
  0.27s user 0.04s system 100% cpu 0.305 total
  0.27s user 0.04s system 99% cpu 0.309 total
Switched to branch 'module-prepend'
Your branch is up to date with 'origin/module-prepend'.
Patch:
  0.18s user 0.03s system 99% cpu 0.217 total
  0.19s user 0.03s system 97% cpu 0.226 total
  0.19s user 0.03s system 97% cpu 0.226 total
  0.19s user 0.03s system 96% cpu 0.232 total
  0.19s user 0.03s system 96% cpu 0.230 total

Module.new:

$ benchmark "1000.times { Module.new }"
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Master:
  0.26s user 0.04s system 99% cpu 0.295 total
  0.26s user 0.04s system 99% cpu 0.298 total
  0.27s user 0.04s system 98% cpu 0.310 total
  0.27s user 0.04s system 99% cpu 0.312 total
  0.26s user 0.04s system 99% cpu 0.295 total
Switched to branch 'module-prepend'
Your branch is up to date with 'origin/module-prepend'.
Patch:
  0.17s user 0.03s system 98% cpu 0.198 total
  0.18s user 0.03s system 97% cpu 0.214 total
  0.20s user 0.03s system 97% cpu 0.236 total
  0.18s user 0.03s system 97% cpu 0.213 total
  0.18s user 0.03s system 96% cpu 0.224 total

Module#include:

$ benchmark "K = Class.new; 1000.times { K.include(Module.new) }"
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Master:
  0.45s user 0.04s system 100% cpu 0.491 total
  0.46s user 0.04s system 100% cpu 0.499 total
  0.46s user 0.04s system 100% cpu 0.499 total
  0.46s user 0.04s system 101% cpu 0.487 total
  0.47s user 0.04s system 100% cpu 0.503 total
Switched to branch 'module-prepend'
Your branch is up to date with 'origin/module-prepend'.
Patch:
  2.67s user 0.04s system 99% cpu 2.710 total
  3.55s user 0.04s system 99% cpu 3.605 total
  5.18s user 0.05s system 99% cpu 5.239 total
  2.42s user 0.04s system 99% cpu 2.483 total
  2.42s user 0.04s system 99% cpu 2.462 total

singleton_class:

$ benchmark "1000.times { Object.new.singleton_class }"
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Master:
  0.26s user 0.04s system 97% cpu 0.302 total
  0.26s user 0.03s system 99% cpu 0.287 total
  0.25s user 0.03s system 100% cpu 0.286 total
  0.26s user 0.03s system 99% cpu 0.295 total
  0.26s user 0.03s system 100% cpu 0.291 total
Switched to branch 'module-prepend'
Your branch is up to date with 'origin/module-prepend'.
Patch:
  0.19s user 0.03s system 99% cpu 0.225 total
  0.20s user 0.03s system 97% cpu 0.238 total
  0.20s user 0.03s system 97% cpu 0.235 total
  0.20s user 0.03s system 97% cpu 0.232 total
  0.20s user 0.03s system 97% cpu 0.236 total

So Module#include seems to be a bottleneck, it's 5-7 times slower. btw, Module#prepend has the same performance (because it does pretty much the same thing)

@iliabylich
Copy link
Contributor Author

iliabylich commented May 17, 2018

Actually it's not that bad when the ancestors chain is small:

$ benchmark "1000.times { Class.new { include Module.new }}"
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
Master:
  0.36s user 0.04s system 103% cpu 0.390 total
  0.37s user 0.04s system 103% cpu 0.392 total
  0.37s user 0.04s system 103% cpu 0.400 total
  0.36s user 0.04s system 103% cpu 0.389 total
  0.36s user 0.04s system 103% cpu 0.387 total
Switched to branch 'module-prepend'
Your branch is up to date with 'origin/module-prepend'.
Patch:
  0.26s user 0.03s system 105% cpu 0.283 total
  0.27s user 0.04s system 104% cpu 0.293 total
  0.26s user 0.04s system 104% cpu 0.288 total
  0.26s user 0.04s system 103% cpu 0.285 total
  0.25s user 0.04s system 104% cpu 0.282 total

Also I've mirco-benchmarked Opal.append_features for this specific case:

  1. 3937.128662109375 μs - getting a list of module's ancestors.
  2. 5101.341552734375 μs - build iclasses.
  3. 6154.453857421875 μs - building a chain of iclasses (i.e. chaining prototypes)
  4. 568.290283203125 μs finding a place in existing prototypes chain to insert a new one
  5. 2381.034912109375 μs - updating module's prototype (i.e. inserting a chain if iclasses)

@ggrossetie
Copy link
Member

@iliabylich I'm wondering why FF is so slow compared to Chrome to run the RubySpec suite ? What version are you using ?

Also the performance of module#include is intriguing... basically it's faster when we include a module only once into a class but it's 5-7 slower when we include 1000 modules to the same class ?
I didn't have time to read the code yet but I guess we are doing checks on the class that's why include is getting slower and slower ?

@iliabylich
Copy link
Contributor Author

What version are you using ?

60.0.1

it's faster when we include a module only once into a class but it's 5-7 slower when we include 1000 modules to the same class ?

exactly.

we are doing checks on the class that's why include is getting slower and slower ?

From what I understand for a case when we have a huge ancestors chain the time is mostly taken by the part that rebuilds a prototype chain. Maybe JS engines do some recalculations when it happens, I don't know. But anyway, that's a rare case, so I think it's fine.

@ggrossetie
Copy link
Member

True. I will run your branch against the Asciidoctor.js benchmark to see if there's a performance gain :)
But as far as I know we are not using prepend anymore.

@elia
Copy link
Member

elia commented May 17, 2018

From what I understand for a case when we have a huge ancestors chain the time is mostly taken by the part that rebuilds a prototype chain. Maybe JS engines do some recalculations when it happens, I don't know. But anyway, that's a rare case, so I think it's fine.

https://jsperf.com/long-proto-chain/1 seems confirmed, the difference with building a proto chain with Object.create is astounding, yet I agree that's fine as it is

@iliabylich
Copy link
Contributor Author

@elia Did you have a chance to check it? The fix for caching $$ancestors is here, can we merge it?

@elia
Copy link
Member

elia commented May 24, 2018

@iliabylich merge at will

@iliabylich iliabylich merged commit 3d46dd4 into opal:master May 25, 2018
@iliabylich iliabylich deleted the module-prepend branch May 25, 2018 08:14
@elia elia mentioned this pull request May 27, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants