-
-
Notifications
You must be signed in to change notification settings - Fork 332
constants system rewrite (in two parts) #1653
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a 👏 . Great work, I would say partially broken constant lookup was a number 1 issue. You are a hero 😄 . Are there any intermediate benchmark results? And did you try to run the rubyspec test suite with those failing random seeds?
add_special :nesting do |compile_default| | ||
recv, method, *args = children | ||
push_nesting = push_nesting?(children) | ||
push '(Opal.Module.$$nesting = $nesting, ' if push_nesting |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it necessary to populate Opal.Module.$$nesting
and call the default behavior? It's a special case for CallNode
anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought that overriding Module.nesting
should be supported and I'd be very curious to see an app that has this as a bottleneck 😄 – Also could be a different Module
than ::Module
.
recv = children.first | ||
|
||
children.size == 2 && ( # only receiver and method | ||
recv.nil? || ( # and no receiver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it could be replaced with recv.nil? || recv == s(:const, nil, :Module)
. Is it written this way to cover both Module
and ::Module
(with nil
and s(:cbase)
as constant scopes)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think I meant to cover both nil
and s(:cbase)
. This part could probably use a test.
def instance_eval(*args, &block) | ||
if block.nil? && `!!Opal.compile` | ||
Kernel.raise ArgumentError, "wrong number of arguments (0 for 1..3)" unless (1..3).cover? args.size | ||
::Kernel.raise ::ArgumentError, "wrong number of arguments (0 for 1..3)" unless (1..3).cover? args.size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it required to put ::
everywhere now to avoid calling local constants? (Like potentially generated on the fly BasicObject::Kernel
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, it's because from inside BasicObject you can't access top level constants… except by making the lookup qualified with the leading double-dots and ignore Module.nesting
.
See also the changes in native.rb
No, still need to start on that side
yes, and there was no error 😸, even before adding all those specs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few small notes, also there's some commented out code in runtime.js that most probably could be removed. 👍 from me even without benchmarks. working correctly is more important than being fast 😄
opal/corelib/runtime.js
Outdated
Opal.constants.push("Object"); | ||
Opal.constants.push("Module"); | ||
Opal.constants.push("Class"); | ||
// BasicObject can reach itself, avoid const_set to skup the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
skip
stdlib/json.rb
Outdated
if (!options.parse && (klass = #{`hash`[JSON.create_id]}) != nil) { | ||
klass = Opal.get(klass); | ||
klass = Opal.const_get_qualified('::', klass); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to rewrite it using const_get
from Ruby? If it's a global constant probably Object.const_get
may work here. For me "::"
looks like a private api from runtime.js
that shouldn't be used explicitly in corelib/stdlib
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@iliabylich I think the important point here is that we should define the public runtime API before 1.0
I'm going to review it this weekend, I did read it already when you pushed the first stuff, all I could think of is the diff is unreadable. EDIT: wait, it was unreadable, or I'm thinking of something else, or whatever, what are you still doing awake? |
@meh I did a bunch of side stuff that was initially in the PR but then I separated and pushed to master, that's probably why now it's more readable 📖 Anyway I'd like to hear some thoughts on this from you before proceeding in trying to optimize for performance, so looking forward to your review ✏️ 👓 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good other than those two nits.
|
||
if compiler.eval? | ||
add_temp '$scope = (self.$$scope || self.$$class.$$scope)' | ||
add_temp '$nesting = self.$$is_a_module ? [self] : [self.$$class]' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be $$is_module
instead of $$is_a_module
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@meh there's $$is_module
and $$is_class
that XOR on modules and classes, but there's also $$is_a_module
that is true
for both and replaces $$is_module || $$is_class
checks.
That said I agree that a better name is desirable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe $$is_namespace
? Since that's what the check is looking for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opal/corelib/module.rb
Outdated
} else { | ||
#{raise NameError, "constant #{self}::#{name} not defined"} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of this would probably look better if you removed the else if { } else { }
since you're returning from each of them.
current status of perf vs. master:
|
c002787
to
e77b07f
Compare
After adding the cache perf go back more than acceptable:
bm_constants_lookup.rb module A
module B
module C
Benchmark.ips do |x|
x.report("Kernel") { Kernel }
x.report("::Kernel"){ ::Kernel }
x.compare!
end
end
end
end |
Checking performance at each commit shows an overall speed improvement from 50% to almost 80% (details in this gist). 🏎 |
Isn't that going to slow down startup considerably, or am I reading it wrong? I can already see @jgaskins yelling at us. |
Some things might be a little slower, but I'd be interested in seeing what this looks like in a real-world app. Because some constants are referenced over and over just during bootstrapping of an app, the extra cost might be amortized. I'll run a benchmark tomorrow and see what I can find out. |
@meh @jgaskins tbh I think the opposite to be true, the caching price is paid only the first time a const is referenced and only if it is referenced, previously it was upfront for any constant, even if never used. Also the cache is invalidated globally for any const change but it's done locally and only for referenced constants. Unless an app is going to keep modifying constants at runtime it will be much better. 😄 |
Okay, quick analysis before bedtime (that's a lie, I actually should've been in bed an hour ago). This is how I usually run load-time benchmarks: <div>
<span>Load time: </span>
<span id="benchmark-result"></span>
</div>
<script>var started_at = performance.now()</script>
<script src="app.js"></script>
<script>document.getElementById('benchmark-result').textContent = (performance.now() - started_at).toString()</script> Then I load the page a bunch of times to get a feel for where the number converges. The app is compiled/minified/gzipped just as you would in production to remove the overhead of multiple requests. The app I chose to test this with was 1543 LoC (not counting blank lines and comments). There are 139 top-level constants. It uses Clearwater, GrandCentral, and Opal::Pusher, which have a few nested constants, but not a whole lot. Maybe 30 across all of them. The benchmark values below only include load time without rendering (Clearwater renders with On Chrome, the numbers converged around 148ms for this PR vs 157ms on the On Safari, it reduced it to 72ms from 78ms (7.7% faster). On Firefox, this was a little slower than This PR also had a lot less fluctuation in the load-time benchmarks than the It seems like a load-time performance win, tbh. Slower in Firefox, but the vast majority of mobile users (where load-time performance matters most) are on Chrome or Safari. I'll try to test a bit more on mobile devices this weekend if I get a chance. |
Still needs some fixes for proper autoloading checks and to go back to acceptable speed. Special thanks to: - The Rails autoloading guide (http://guides.rubyonrails.org/v5.0/autoloading_and_reloading_constants.html) - @ConradIrwin's 2012 post on “Everything you ever wanted to know about constant lookup in Ruby” (http://cirw.in/blog/constant-lookup.html)
- It will expire every time a constant is defined, removed or set. - For relative lookups the cache will be held on the current nesting. - For qualified lookups the cache will be held on the current cref. - Only the referenced constants will be cached. There's a slight risk of memory leak if constants are set dynamically and their unreferenced, but seems a good overall compromise that can only be resolved by a WeakRef JS implementation.
This follows the fix done in opal/opal#1653 (constants system rewrite)
🙅♂️ DO NOT MERGE YETa complete rewrite of the constants system to actually mimic CRuby behavior
To do before merging:
Part 1: Make it correct ✅
This is more or less done:
Module.nesting
andModule.constants
correctly workBasicObject
behaves properly (see [edge-case] constant lookup diverge from MRI #548)Part 2: Make it fast ⏳
This is still to be done, some considerations:
$nesting
Opal.get