Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Named based constant cache invalidation #798

Merged
merged 8 commits into from

3 participants

@charliesome

Hi!

I've implemented named based constant cache invalidation in JRuby.

In my implementation, I've added a hashtable mapping constant names to Invalidator objects on the Ruby class. The lookup of the Invalidator object is cached in fields of certain classes (example) - I've tried to make this threadsafe, but I'd like my approach verified because I've probably made a mistake somewhere.

I used this script to benchmark the performance improvement of this patch:

require "benchmark"

class A
  Foo = 123

  class B
    class C
      class D
        class E
          def self.get_foo
            Foo
          end
        end
      end
    end
  end
end

Bar = 456

5.times do
  puts Benchmark.measure {
    1_000_000.times do
      Object.send(:remove_const, :Bar)
      Object.const_set(:Bar, 456)
      A::B::C::D::E.get_foo
    end
  }
end

ruby-2.0.0-p0:

  0.930000   0.000000   0.930000 (  0.935408)
  0.940000   0.000000   0.940000 (  0.937916)
  0.970000   0.000000   0.970000 (  0.967160)
  0.940000   0.000000   0.940000 (  0.937358)
  0.930000   0.000000   0.930000 (  0.931662)

JRuby master:

  1.250000   0.010000   1.260000 (  0.865000)
  0.700000   0.000000   0.700000 (  0.670000)
  0.700000   0.010000   0.710000 (  0.686000)
  0.670000   0.000000   0.670000 (  0.677000)
  0.680000   0.000000   0.680000 (  0.679000)

JRuby constant-name-cache:

  1.250000   0.010000   1.260000 (  0.794000)
  0.620000   0.000000   0.620000 (  0.589000)
  0.580000   0.010000   0.590000 (  0.572000)
  0.570000   0.000000   0.570000 (  0.568000)
  0.570000   0.010000   0.580000 (  0.570000)

:sparkles:

@headius
Owner

Mostly good suggestions from @wmeissner. Summing up some points:

  1. ConcurrentHashMap would be good. It will be mostly read, so we can set it to concurrency level of 1 and it won't be too big then.
  2. When a new invalidator is created, appropriate atomic operations must be performed to guarantee exactly one is ever created. If two get created, you could see threads stomp on each other and someone cache an invalidator that will never be invalidated. The synchronized modifier on getConstantValidator will do this, but we don't want to synchronize on every read either. CHM.putIfAbsent should do the trick. (Note: This may apply to your Rubinius patch as well).
  3. I don't think the invalidator fields need to be volatile; worst case is they get set by multiple threads, but as long as (2) holds, they'll just end up with the same invalidator anyway. And CHM will guarantee volatility of the invalidator state (happens-before).

We can attempt to make these changes for you, or if you like you can try them yourself. They're not major, and the patch looks good otherwise.

@charliesome

@wmeissner @headius Thanks for the fantastic feedback!

I've gone ahead and implemented your suggestions, also I've removed the synchronization from the invalidator accessor methods on the AST and IR classes because I figure if multiple threads race on this, the worst that can happen is invalidator is set twice. Let me know what you think.

@headius
Owner

Looks good! I'll get this merged and play with it a bit.

One note about the benchmark: We don't handle caching Colon2 as well as we could, so there could be considerably more improvement if we did that. Your improvement here could make the entire :: chain optimize away to nothing and never invalidate...so that's motivation for me to make another attempt.

@jrubyci jrubyci merged commit 79dad3e into jruby:master
@headius
Owner

Merged in 79dad3e. Thanks! Will have to try improving the perf of Colon2 logic in the compiler now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
34 src/org/jruby/Ruby.java
@@ -135,6 +135,7 @@
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
@@ -219,7 +220,6 @@ private Ruby(RubyInstanceConfig config) {
this.runtimeCache = new RuntimeCache();
runtimeCache.initMethodCache(ClassIndex.MAX_CLASSES * MethodNames.values().length - 1);
- constantInvalidator = OptoFactory.newConstantInvalidator();
checkpointInvalidator = OptoFactory.newConstantInvalidator();
if (config.isObjectSpaceEnabled()) {
@@ -4181,23 +4181,28 @@ public int getConstantGeneration() {
return -1;
}
- @Deprecated
- public synchronized void incrementConstantGeneration() {
- constantInvalidator.invalidate();
+ public Invalidator getConstantInvalidator(String constantName) {
+ Invalidator invalidator = constantNameInvalidators.get(constantName);
+ if (invalidator != null) {
+ return invalidator;
+ } else {
+ return addConstantInvalidator(constantName);
+ }
}
-
- public Invalidator getConstantInvalidator() {
- return constantInvalidator;
+
+ private Invalidator addConstantInvalidator(String constantName) {
+ Invalidator invalidator = OptoFactory.newConstantInvalidator();
+ constantNameInvalidators.putIfAbsent(constantName, invalidator);
+
+ // fetch the invalidator back from the ConcurrentHashMap to ensure that
+ // only one invalidator for a given constant name is ever used:
+ return constantNameInvalidators.get(constantName);
}
public Invalidator getCheckpointInvalidator() {
return checkpointInvalidator;
}
- public void invalidateConstants() {
-
- }
-
public <E extends Enum<E>> void loadConstantSet(RubyModule module, Class<E> enumClass) {
for (E e : EnumSet.allOf(enumClass)) {
Constant c = (Constant) e;
@@ -4467,7 +4472,12 @@ public CallbackFactory callbackFactory(Class<?> type) {
throw new RuntimeException("callback-style handles are no longer supported in JRuby");
}
- private final Invalidator constantInvalidator;
+ private final ConcurrentHashMap<String, Invalidator> constantNameInvalidators =
+ new ConcurrentHashMap<String, Invalidator>(
+ 16 /* default initial capacity */,
+ 0.75f /* default load factory */,
+ 1 /* concurrency level - mostly reads here so this can be 1 */);
+
private final Invalidator checkpointInvalidator;
private final ThreadService threadService;
View
45 src/org/jruby/RubyModule.java
@@ -986,9 +986,8 @@ private CacheEntry cacheHit(String name) {
private void invalidateConstantCacheForModuleInclusion(RubyModule module)
{
for (RubyModule mod : gatherModules(module)) {
- if (!mod.getConstantMap().isEmpty()) {
- invalidateConstantCache();
- break;
+ for (String key : mod.getConstantMap().keySet()) {
+ invalidateConstantCache(key);
}
}
}
@@ -1144,8 +1143,8 @@ protected void invalidateCacheDescendantsInner() {
methodInvalidator.invalidate();
}
- protected void invalidateConstantCache() {
- getRuntime().getConstantInvalidator().invalidate();
+ protected void invalidateConstantCache(String constantName) {
+ getRuntime().getConstantInvalidator(constantName).invalidate();
}
/**
@@ -2619,7 +2618,7 @@ public IRubyObject remove_const(ThreadContext context, IRubyObject rubyName) {
String name = validateConstant(rubyName.asJavaString());
IRubyObject value;
if ((value = deleteConstant(name)) != null) {
- invalidateConstantCache();
+ invalidateConstantCache(name);
if (value != UNDEF) {
return value;
}
@@ -2731,34 +2730,38 @@ public RubyArray constantsCommon19(ThreadContext context, boolean replaceModule,
}
@JRubyMethod(compat = RUBY1_9)
- public IRubyObject private_constant(ThreadContext context, IRubyObject name) {
- setConstantVisibility(context, validateConstant(name.asJavaString()), true);
- invalidateConstantCache();
+ public IRubyObject private_constant(ThreadContext context, IRubyObject rubyName) {
+ String name = rubyName.asJavaString();
+ setConstantVisibility(context, validateConstant(name), true);
+ invalidateConstantCache(name);
return this;
}
@JRubyMethod(compat = RUBY1_9, required = 1, rest = true)
- public IRubyObject private_constant(ThreadContext context, IRubyObject[] names) {
- for (IRubyObject name : names) {
- setConstantVisibility(context, validateConstant(name.asJavaString()), true);
+ public IRubyObject private_constant(ThreadContext context, IRubyObject[] rubyNames) {
+ for (IRubyObject rubyName : rubyNames) {
+ String name = rubyName.asJavaString();
+ setConstantVisibility(context, validateConstant(name), true);
+ invalidateConstantCache(name);
}
- invalidateConstantCache();
return this;
}
@JRubyMethod(compat = RUBY1_9)
- public IRubyObject public_constant(ThreadContext context, IRubyObject name) {
- setConstantVisibility(context, validateConstant(name.asJavaString()), false);
- invalidateConstantCache();
+ public IRubyObject public_constant(ThreadContext context, IRubyObject rubyName) {
+ String name = rubyName.asJavaString();
+ setConstantVisibility(context, validateConstant(name), false);
+ invalidateConstantCache(name);
return this;
}
@JRubyMethod(compat = RUBY1_9, required = 1, rest = true)
- public IRubyObject public_constant(ThreadContext context, IRubyObject[] names) {
- for (IRubyObject name : names) {
- setConstantVisibility(context, validateConstant(name.asJavaString()), false);
+ public IRubyObject public_constant(ThreadContext context, IRubyObject[] rubyNames) {
+ for (IRubyObject rubyName : rubyNames) {
+ String name = rubyName.asJavaString();
+ setConstantVisibility(context, validateConstant(name), false);
+ invalidateConstantCache(name);
}
- invalidateConstantCache();
return this;
}
@@ -3082,7 +3085,7 @@ private IRubyObject setConstantCommon(String name, IRubyObject value, boolean wa
storeConstant(name, value);
}
- invalidateConstantCache();
+ invalidateConstantCache(name);
// if adding a module under a constant name, set that module's basename to the constant name
if (value instanceof RubyModule) {
View
4 src/org/jruby/ast/Colon2ConstNode.java
@@ -65,12 +65,12 @@ private boolean isCached(ThreadContext context, RubyModule target, IRubyObject v
// We could probably also detect if LHS value came out of cache and avoid some of this
return
value != null &&
- generation == context.runtime.getConstantInvalidator().getData() &&
+ generation == invalidator(context).getData() &&
hash == target.hashCode();
}
public IRubyObject reCache(ThreadContext context, RubyModule target) {
- Object newGeneration = context.runtime.getConstantInvalidator().getData();
+ Object newGeneration = invalidator(context).getData();
IRubyObject value = target.getConstantFromNoConstMissing(name, false);
cachedValue = value;
View
14 src/org/jruby/ast/Colon3Node.java
@@ -43,6 +43,7 @@
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.runtime.opto.Invalidator;
import org.jruby.util.ByteList;
import org.jruby.util.DefinedMessage;
@@ -54,6 +55,7 @@
protected String name;
private volatile transient IRubyObject cachedValue;
private volatile Object generation;
+ private Invalidator invalidator;
public Colon3Node(ISourcePosition position, String name) {
super(position);
@@ -86,6 +88,7 @@ public String getName() {
public void setName(String name) {
this.name = name;
+ this.invalidator = null;
}
/** Get parent module/class that this module represents */
@@ -132,12 +135,12 @@ public IRubyObject getValue(ThreadContext context) {
}
private boolean isCached(ThreadContext context, IRubyObject value) {
- return value != null && generation == context.runtime.getConstantInvalidator().getData();
+ return value != null && generation == invalidator(context).getData();
}
public IRubyObject reCache(ThreadContext context, String name) {
Ruby runtime = context.runtime;
- Object newGeneration = runtime.getConstantInvalidator().getData();
+ Object newGeneration = invalidator(context).getData();
IRubyObject value = runtime.getObject().getConstantFromNoConstMissing(name, false);
cachedValue = value;
@@ -146,4 +149,11 @@ public IRubyObject reCache(ThreadContext context, String name) {
return value;
}
+
+ protected Invalidator invalidator(ThreadContext context) {
+ if (invalidator == null) {
+ invalidator = context.runtime.getConstantInvalidator(name);
+ }
+ return invalidator;
+ }
}
View
15 src/org/jruby/ast/ConstNode.java
@@ -41,6 +41,7 @@
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.runtime.opto.Invalidator;
import org.jruby.util.ByteList;
import org.jruby.util.DefinedMessage;
@@ -51,6 +52,7 @@
private String name;
private volatile transient IRubyObject cachedValue = null;
private Object generation = -1;
+ private Invalidator invalidator;
public ConstNode(ISourcePosition position, String name) {
super(position);
@@ -83,6 +85,7 @@ public String getName() {
public void setName(String name) {
this.name = name;
+ this.invalidator = null;
}
@Override
@@ -106,12 +109,13 @@ public IRubyObject getValue(ThreadContext context) {
}
private boolean isCached(ThreadContext context, IRubyObject value) {
- return value != null && generation == context.runtime.getConstantInvalidator().getData();
+ return value != null && generation == invalidator(context).getData();
}
public IRubyObject reCache(ThreadContext context, String name) {
- Object newGeneration = context.runtime.getConstantInvalidator().getData();
+ Object newGeneration = invalidator(context).getData();
IRubyObject value = context.getCurrentStaticScope().getConstant(name);
+ this.name = name;
cachedValue = value;
@@ -119,4 +123,11 @@ public IRubyObject reCache(ThreadContext context, String name) {
return value;
}
+
+ private Invalidator invalidator(ThreadContext context) {
+ if (invalidator == null) {
+ invalidator = context.runtime.getConstantInvalidator(name);
+ }
+ return invalidator;
+ }
}
View
14 src/org/jruby/ast/executable/RuntimeCache.java
@@ -25,6 +25,7 @@
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.callsite.CacheEntry;
+import org.jruby.runtime.opto.Invalidator;
import org.jruby.util.ByteList;
import org.jruby.util.DefinedMessage;
import org.jruby.util.RegexpOptions;
@@ -375,6 +376,7 @@ public final void initConstants(int size) {
constants = new IRubyObject[size];
constantTargetHashes = new int[size];
constantGenerations = new Object[size];
+ constantInvalidators = new Invalidator[size];
Arrays.fill(constantGenerations, -1);
Arrays.fill(constantTargetHashes, -1);
}
@@ -411,13 +413,15 @@ public IRubyObject getValue(ThreadContext context, StaticScope scope, String nam
}
private boolean isCached(ThreadContext context, IRubyObject value, int index) {
- return value != null && constantGenerations[index] == context.runtime.getConstantInvalidator().getData();
+ return value != null && constantGenerations[index] == constantInvalidators[index].getData();
}
public IRubyObject reCache(ThreadContext context, StaticScope scope, String name, int index) {
- Object newGeneration = context.runtime.getConstantInvalidator().getData();
+ Invalidator invalidator = context.runtime.getConstantInvalidator(name);
+ Object newGeneration = invalidator.getData();
IRubyObject value = scope.getConstant(name);
constants[index] = value;
+ constantInvalidators[index] = invalidator;
if (value != null) {
constantGenerations[index] = newGeneration;
}
@@ -436,16 +440,17 @@ public IRubyObject getValueFrom(RubyModule target, ThreadContext context, String
}
private boolean isCachedFrom(RubyModule target, ThreadContext context, IRubyObject value, int index) {
- return value != null && constantGenerations[index] == context.runtime.getConstantInvalidator().getData() && constantTargetHashes[index] == target.hashCode();
+ return value != null && constantGenerations[index] == constantInvalidators[index].getData() && constantTargetHashes[index] == target.hashCode();
}
public IRubyObject reCacheFrom(RubyModule target, ThreadContext context, String name, int index) {
- Object newGeneration = context.runtime.getConstantInvalidator().getData();
+ Object newGeneration = context.runtime.getConstantInvalidator(name).getData();
IRubyObject value = target.getConstantFromNoConstMissing(name, false);
constants[index] = value;
if (value != null) {
constantGenerations[index] = newGeneration;
constantTargetHashes[index] = target.hashCode();
+ constantInvalidators[index] = context.runtime.getConstantInvalidator(name);
}
return value;
}
@@ -673,4 +678,5 @@ private CacheEntry getCacheEntry(int index) {
private static final Object[] EMPTY_OBJS = {};
public Object[] constantGenerations = EMPTY_OBJS;
public int[] constantTargetHashes = EMPTY_INTS;
+ public Invalidator[] constantInvalidators = {};
}
View
2  src/org/jruby/embed/variable/Argv.java
@@ -132,7 +132,7 @@ public void inject() {
if (rubyModule == null) return;
rubyModule.storeConstant(name, irubyObject);
- receiver.getRuntime().getConstantInvalidator().invalidate();
+ receiver.getRuntime().getConstantInvalidator(name).invalidate();
fromRuby = true;
}
View
2  src/org/jruby/embed/variable/Constant.java
@@ -225,7 +225,7 @@ public void inject() {
} else {
receiver.getMetaClass().storeConstant(name, irubyObject);
}
- receiver.getRuntime().getConstantInvalidator().invalidate();
+ receiver.getRuntime().getConstantInvalidator(name).invalidate();
initialized = true;
}
View
13 src/org/jruby/ir/instructions/InheritanceSearchConstInstr.java
@@ -12,6 +12,7 @@
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.runtime.opto.Invalidator;
import java.util.Map;
@@ -30,6 +31,7 @@
private volatile transient Object cachedConstant = null;
private volatile int hash = -1;
private volatile Object generation = -1;
+ private Invalidator invalidator;
public InheritanceSearchConstInstr(Variable result, Operand currentModule, String constName, boolean noPrivateConsts) {
super(Operation.INHERITANCE_SEARCH_CONST);
@@ -76,7 +78,7 @@ private Object cache(Ruby runtime, RubyModule module) {
constant = UndefinedValue.UNDEFINED;
} else {
// recache
- generation = runtime.getConstantInvalidator().getData();
+ generation = runtime.getConstantInvalidator(constName).getData();
hash = module.hashCode();
cachedConstant = constant;
}
@@ -84,7 +86,7 @@ private Object cache(Ruby runtime, RubyModule module) {
}
private boolean isCached(Ruby runtime, RubyModule target, Object value) {
- return value != null && generation == runtime.getConstantInvalidator().getData() && hash == target.hashCode();
+ return value != null && generation == invalidator(runtime).getData() && hash == target.hashCode();
}
@Override
@@ -119,4 +121,11 @@ public String getConstName() {
public boolean isNoPrivateConsts() {
return noPrivateConsts;
}
+
+ private Invalidator invalidator(Ruby runtime) {
+ if (invalidator == null) {
+ invalidator = runtime.getConstantInvalidator(constName);
+ }
+ return invalidator;
+ }
}
View
13 src/org/jruby/ir/instructions/LexicalSearchConstInstr.java
@@ -13,6 +13,7 @@
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.runtime.opto.Invalidator;
import java.util.Map;
@@ -29,6 +30,7 @@
// Constant caching
private volatile transient Object cachedConstant = null;
private Object generation = -1;
+ private Invalidator invalidator;
public LexicalSearchConstInstr(Variable result, Operand definingScope, String constName) {
super(Operation.LEXICAL_SEARCH_CONST);
@@ -78,14 +80,14 @@ private Object cache(ThreadContext context, DynamicScope currDynScope, IRubyObje
constant = UndefinedValue.UNDEFINED;
} else {
// recache
- generation = runtime.getConstantInvalidator().getData();
+ generation = invalidator(runtime).getData();
cachedConstant = constant;
}
return constant;
}
private boolean isCached(Ruby runtime, Object value) {
- return value != null && generation == runtime.getConstantInvalidator().getData();
+ return value != null && generation == invalidator(runtime).getData();
}
@Override
@@ -101,4 +103,11 @@ public Object interpret(ThreadContext context, DynamicScope currDynScope, IRubyO
public void visit(IRVisitor visitor) {
visitor.LexicalSearchConstInstr(this);
}
+
+ private Invalidator invalidator(Ruby runtime) {
+ if (invalidator == null) {
+ invalidator = runtime.getConstantInvalidator(constName);
+ }
+ return invalidator;
+ }
}
View
13 src/org/jruby/ir/instructions/SearchConstInstr.java
@@ -12,6 +12,7 @@
import org.jruby.runtime.DynamicScope;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
+import org.jruby.runtime.opto.Invalidator;
import java.util.Map;
@@ -28,6 +29,7 @@
// Constant caching
private volatile transient Object cachedConstant = null;
private Object generation = -1;
+ private Invalidator invalidator;
public SearchConstInstr(Variable result, String constName, Operand startingScope, boolean noPrivateConsts) {
super(Operation.SEARCH_CONST);
@@ -85,7 +87,7 @@ private Object cache(ThreadContext context, DynamicScope currDynScope, IRubyObje
constant = module.callMethod(context, "const_missing", context.runtime.fastNewSymbol(constName));
} else {
// recache
- generation = runtime.getConstantInvalidator().getData();
+ generation = runtime.getConstantInvalidator(constName).getData();
cachedConstant = constant;
}
@@ -93,7 +95,7 @@ private Object cache(ThreadContext context, DynamicScope currDynScope, IRubyObje
}
private boolean isCached(Ruby runtime, Object value) {
- return value != null && generation == runtime.getConstantInvalidator().getData();
+ return value != null && generation == invalidator(runtime).getData();
}
@Override
@@ -121,4 +123,11 @@ public String getConstName() {
public boolean isNoPrivateConsts() {
return noPrivateConsts;
}
+
+ private Invalidator invalidator(Ruby runtime) {
+ if (invalidator == null) {
+ invalidator = runtime.getConstantInvalidator(constName);
+ }
+ return invalidator;
+ }
}
View
4 src/org/jruby/ir/targets/Bootstrap.java
@@ -767,7 +767,7 @@ public static boolean testType(RubyClass original, ThreadContext context, IRubyO
public static IRubyObject searchConst(MutableCallSite site, String constName, ThreadContext context, StaticScope staticScope) throws Throwable {
Ruby runtime = context.runtime;
- SwitchPoint switchPoint = (SwitchPoint)runtime.getConstantInvalidator().getData();
+ SwitchPoint switchPoint = (SwitchPoint)runtime.getConstantInvalidator(constName).getData();
IRubyObject value = staticScope.getConstant(constName);
if (value == null) {
@@ -797,7 +797,7 @@ public static IRubyObject inheritanceSearchConst(MutableCallSite site, String co
throw runtime.newTypeError(cmVal + " is not a type/class");
}
- SwitchPoint switchPoint = (SwitchPoint)runtime.getConstantInvalidator().getData();
+ SwitchPoint switchPoint = (SwitchPoint)runtime.getConstantInvalidator(constName).getData();
IRubyObject value = module.getConstantFromNoConstMissing(constName, false);
if (value == null) {
View
6 src/org/jruby/runtime/invokedynamic/InvokeDynamicSupport.java
@@ -760,7 +760,7 @@ public static void checkpointFallback(MutableCallSite site, ThreadContext contex
public static IRubyObject constantFallback(RubyConstantCallSite site,
AbstractScript script, ThreadContext context, int scopeIndex) {
- SwitchPoint switchPoint = (SwitchPoint)context.runtime.getConstantInvalidator().getData();
+ SwitchPoint switchPoint = (SwitchPoint)context.runtime.getConstantInvalidator(site.name()).getData();
StaticScope scope = script.getScope(scopeIndex);
IRubyObject value = scope.getConstant(site.name());
@@ -789,7 +789,7 @@ public static IRubyObject constantFallback(RubyConstantCallSite site,
public static boolean constantBooleanFallback(RubyConstantCallSite site,
AbstractScript script, ThreadContext context, int scopeIndex) {
- SwitchPoint switchPoint = (SwitchPoint)context.runtime.getConstantInvalidator().getData();
+ SwitchPoint switchPoint = (SwitchPoint)context.runtime.getConstantInvalidator(site.name()).getData();
StaticScope scope = script.getScope(scopeIndex);
IRubyObject value = scope.getConstant(site.name());
@@ -993,4 +993,4 @@ public static MethodHandle findVirtual(Class target, String name, MethodType typ
private static String extractSourceInfo(VariableSite site) {
return " (" + site.file() + ":" + site.line() + ")";
}
-}
+}
Something went wrong with that request. Please try again.