-
-
Notifications
You must be signed in to change notification settings - Fork 925
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
Block call IR optimization failures by 0d35995e1704bb716a9b54064208cf82ace2f150 #3577
Comments
This looks specific to JIT mode. I forced a full build of scopes to generate explicit call protocol for blocks in -X-C mode and it doesn't fail. Below is a shorter snippet I could reduce this to (but not yet an independent standalone test case). To be continued. /cc @headius require File.expand_path('helper', File.dirname(__FILE__))
require 'tmpdir'
module TestHTTPCookieJar
class WithHashStore < Test::Unit::TestCase
def setup(options = nil, options2 = nil)
default_options = {
:store => :hash,
:gc_threshold => 1500, # increased by 10 for shorter test time
}
new_options = default_options.merge(options || {})
@jar = HTTP::CookieJar.new(new_options)
end
def cookie_values(options = {})
{
:name => 'Foo',
:value => 'Bar',
:path => '/',
:expires => Time.at(Time.now.to_i + 10 * 86400), # to_i is important here
:for_domain => true,
:domain => 'rubyforge.org',
:origin => 'http://rubyforge.org/'
}.merge(options)
end
def test_add_multiple_cookies_with_the_same_name
now = Time.now
cookies = [
{ :value => 'a', :path => '/', },
{ :value => 'b', :path => '/abc/def/', :created_at => now - 1 },
{ :value => 'c', :path => '/abc/def/', :domain => 'www.rubyforge.org', :origin => 'http://www.rubyforge.org/abc/def/', :created_at => now },
{ :value => 'd', :path => '/abc/' },
].map { |attrs|
HTTP::Cookie.new(cookie_values(attrs))
}
url = URI 'http://www.rubyforge.org/abc/def/ghi'
cookies.permutation(cookies.size) { |shuffled|
@jar.clear
shuffled.each { |cookie| @jar.add(cookie) }
@jar.each(url) { |cookie| cookie.value }
}
end
end
end |
Consider this snippet from http/cookie_jar/hash_store.rb def each(uri = nil) # :yield: cookie
now = Time.now
if uri
thost = DomainName.new(uri.host)
tpath = uri.path
@jar.each { |domain, paths|
next unless thost.cookie_domain?(domain)
paths.each { |path, hash|
next unless HTTP::Cookie.path_match?(path, tpath)
hash.delete_if { |name, cookie|
if cookie.expired?(now)
true
else
if cookie.valid_for_uri?(uri)
cookie.accessed_at = now
yield cookie
end
false
end
}
}
}
... That yield statement is throwing an error in the JIT because the self-block is a NULL_BLOCK. So, in the direct yield path somewhere, the self-block is being lost (or is not being loaded properly). |
Everything seems to check out as far as I can tell. I'll let @headius investigate further. |
Looking into this today. |
I believe I have a fairly narrow reproduction now. @ary = [1]
def foo
x = nil
loop {
x = (a = nil; a) or break
x = @ary[0]
}
end;
def bar;
foo
1.times {|x| yield }
end
bar { } I suspect the "or break" is not being handled right and causing a frame to be left on the stack, so that when we return to the I'm not sure why this would be different in JIT, though. |
Slightly smaller: def foo
x = nil
loop {
(a = nil; a) or break
x = x[0]
}
end;
def bar;
foo
loop {|x| yield }
end
bar { } Here's the final optimized IR for the closure in foo:
Both BB4 and BB6 show rescues in the GEB BB7, so it seems like block protocol is ok here. However there's also a |
I did not work on this at all so I don't know what is wrong but I am surprised that rethrow_saved_exc_in_lambda does not have OpFlags.f_can_raise_exception or OpFlags.f_is_exception for it's operation. It is suppose to throw right? @subbuss |
I do not see that ACP even checks for f_can_raise_exception or f_is_exception. It seems like it would be more robust if, instead of guessing at scope type and "backing up" it just iterated through instructions until the first "potential exit" from the block. That would be where to add the protocol. I've got a patch that works for this case, but I'm not sure what else it might affect yet: diff --git a/core/src/main/java/org/jruby/ir/passes/AddCallProtocolInstructions.java b/core/src/main/java/org/jruby/ir/passes/AddCallProtocolInstructions.java
index ad04604..facd40d 100644
--- a/core/src/main/java/org/jruby/ir/passes/AddCallProtocolInstructions.java
+++ b/core/src/main/java/org/jruby/ir/passes/AddCallProtocolInstructions.java
@@ -135,41 +135,41 @@ public class AddCallProtocolInstructions extends CompilerPass {
//
// Allocate GEB if necessary for popping
BasicBlock geb = cfg.getGlobalEnsureBB();
boolean gebProcessed = false;
if (geb == null) {
Variable exc = scope.createTemporaryVariable();
geb = new BasicBlock(cfg, Label.getGlobalEnsureBlockLabel());
geb.addInstr(new ReceiveJRubyExceptionInstr(exc)); // JRuby Implementation exception handling
geb.addInstr(new ThrowExceptionInstr(exc));
cfg.addGlobalEnsureBB(geb);
}
// Pop on all scope-exit paths
for (BasicBlock bb: cfg.getBasicBlocks()) {
Instr i = null;
ListIterator<Instr> instrs = bb.getInstrs().listIterator();
while (instrs.hasNext()) {
i = instrs.next();
// Breaks & non-local returns in blocks will throw exceptions
// and pops for them will be handled in the GEB
- if (!bb.isExitBB() && i instanceof ReturnInstr) {
+ if (!bb.isExitBB() && i instanceof ReturnInstr || i instanceof RethrowSavedExcInLambdaInstr) {
if (requireBinding) fixReturn(scope, (ReturnInstr)i, instrs);
// Add before the break/return
instrs.previous();
popSavedState(scope, bb == geb, requireBinding, requireFrame, savedViz, savedFrame, instrs);
if (bb == geb) gebProcessed = true;
break;
}
}
if (bb.isExitBB() && !bb.isEmpty()) {
// Last instr could be a return -- so, move iterator one position back
if (i != null && i instanceof ReturnInstr) {
if (requireBinding) fixReturn(scope, (ReturnInstr)i, instrs);
instrs.previous();
}
popSavedState(scope, bb == geb, requireBinding, requireFrame, savedViz, savedFrame, instrs);
if (bb == geb) gebProcessed = true;
} else if (!gebProcessed && bb == geb) {
// Add before throw-exception-instr which would be the last instr
if (i != null) { |
Note that private void popSavedState(IRScope scope, boolean isGEB, boolean requireBinding, boolean requireFrame, Variable savedViz, Variable savedFrame, ListIterator<Instr> instrs) {
if (scope instanceof IRClosure && isGEB) {
// Add before RethrowSavedExcInLambdaInstr
instrs.previous();
}
if (requireBinding) instrs.add(new PopBindingInstr());
if (scope instanceof IRClosure) {
instrs.add(new RestoreBindingVisibilityInstr(savedViz));
instrs.add(new PopBlockFrameInstr(savedFrame));
} else {
if (requireFrame) instrs.add(new PopMethodFrameInstr());
}
} |
Pasting IRC comments since I will be moving in/out of irc ..
@headius your fix may not handle all cases. But, in any case, yes, redoing the code in AddACP to add the pops in all the required places a bit more robustly would be a good thing. Note that in the RethrowSavedExcInLambda case, there is a return that shows up after it .. so, the pops shouldn't be added twice. |
@enebo yes .. a bug reg. missing flags as well. |
Temporary fix for #3577. The longer term fix will be to make the instruction insert logic in AddCallProtocol less sensitive to the contents of exit blocks.
Thanks for investigating it. I'd learn the internal from it :-) But with the fix b56f3ca my repro still fails. This test includes something uncovered?
|
I think it's more likely that my fix for the fix unfixed the fix :-( I'll spend more time on it tonight or tomorrow and get it right. |
The problems in #3577 stemmed from the introduction of a second exit instruction into global ensure blocks (rethrow saved exc). Logic elsewhere in our passes that dealt with the GEB all assumed that there would be exactly one control-transfer instruction at the end of the GEB, and inserted various logic before this point: * Stores of modified variables into the current binding. * Pops of binding and frame at the end of the method body. Once there were two instructions, it becamse a mess to know where to insert these instructions. This change modifies the "rethrow" instruction to itself be a special case of "return" that may throw a saved exception. In both cases, it is unrolling at least the current stack frame, and this allows us to have a single instruction to do both normal returns and saved exception rethrows in the GEB. This change reverts previous fixes for #3577 that attempted to work around the double-exit.
Ok, I made a new fix by removing my changes and modifying @subbu's "rethrow" change from 0d35995. All these problem stemmed from there being two exits from the GEB and none of our GEB logic handling that right (because they're all fragile and make assumptions about the structure of the GEB). My change was to combine "return" and "rethrow" into a single instruction so these assumptions would still be true. This still doesn't feel like the long term fix. All these places that do "instruction math" to splice in additional code need to be reevaluated and made more robust in case we have other exits we need to add. Alternatively, we could say that there should only ever be one exit from a GEB and enforce that somehow. @subbuss @enebo Obviously looking forward to your review of this new fix. |
@nahi Please retest with JRuby master. It passes your tests again for me and I confirmed (via |
@headius .. I like this fix. |
I found a test began failing after 0d35995 (cc @subbuss)
Repro:
With 8a04861 it passes so I guess 0d35995 is a cause but I don't have enough knowledge of IR optimization.
The text was updated successfully, but these errors were encountered: