Skip to content

Enable more traces and reduce costs of others#7673

Merged
headius merged 8 commits intojruby:masterfrom
headius:always_on_traces
Mar 14, 2023
Merged

Enable more traces and reduce costs of others#7673
headius merged 8 commits intojruby:masterfrom
headius:always_on_traces

Conversation

@headius
Copy link
Member

@headius headius commented Feb 18, 2023

This PR is an attempt to make more of the trace events (for TracePoint and set_trace_func) always-on, without introducing significant overhead if they are not active (i.e. no enabled tracepoint or trace func).

Raising an exception is already a very heavyweight object. By
making this always-on, we have fewer trace events that require the
--debug flag to capture.

Note that this currently acquires a single caller frame in order
to provide the file and line in Ruby code where the exception was
triggered. This will be of moderate cost on Java 9+ using the
StackWalker API, but will require generating a full trace on Java
8. This may impact exception-raising performance, but only when
hooks are enabled.
This was marked as needing the frame, but it uses the block's
position information to provide file and line rather than getting
it from a caller frame.
This change makes the :call, :return, :b_call, and :b_return hook
events always available, with zero cost after the JVM has run its
optimizations. The enabled and disabled state of these hooks are
controlled by an indy MutableCallSite, which normally points at
a no-op handle but can be switched to a hook-calling handle. When
no hook is active, this will be optimized away in the native code
and have no effect on performance.

There is a small cost here, in that all method and block bodies
will now emit bytecodes to push the context and current module
(or current block) onto the stack, in order to indy dispatch to
the appropriate call or return operation. This increases bytecode
size of all such bodies by 6 bytescodes totalling perhaps 16
bytes, potentially pushing some methods over JVM budgets for JIT
or inlining.
Utility method here avoids making three calls from JIT code.
@headius headius force-pushed the always_on_traces branch 2 times, most recently from e55e5b5 to c7c7f18 Compare February 18, 2023 11:31
This avoids needing a frame to get the frame class.
@headius headius added this to the JRuby 9.4.3.0 milestone Feb 18, 2023
Co-authored-by: Karol Bucek <kares@users.noreply.github.com>
@headius
Copy link
Member Author

headius commented Mar 14, 2023

This was a good experiment, but ultimately we will not enable always-on tracing just yet for a few reasons:

  • This does not include C_CALL and C_RETURN. Those events could be supported via indy, by wrapping native Java method calls with trace hook calls, but it adds another MutableCallSite wrapping the bound handles and increases warmup time.
  • Additionally, C_CALL and C_RETURN cannot be supported efficiently without indy, since they require the caller filename and line number which are not passed into the DynamicMethod.call stubs. They might be able to use StackWalker to get the caller frame.
  • The CALL, RETURN, B_CALL, and B_RETURN events add extra bytecode to all method and block entries and returns, and we already have a tight bytecode budget to get the JVM to inline and optimized method calls. The extra bytecode should boil away to nothing after JVM JIT optimizes, but this adds time to warmup and will be slower than before until that optimization happens.

I am doing one additional commit that will move the IR trace instructions for CALL, RETURN, B_CALL, and B_RETURN back behind the --debug flag. We do get the following benefits out of this PR:

  • Trace event and hook management is moved out of Ruby.java into a new class TraceEventManager.
  • Block frame class access (used for e.g. super calls) now has a utility method, reducing bytecode by two invokevirtuals.
  • Trace instructions are much more efficient in jitted code than before, utilizing indy and better frame access.

@headius
Copy link
Member Author

headius commented Mar 14, 2023

The early non-working prototype of the C_CALL indy wrapper is provided below. The basic idea is to fold the target call into a CallSite for C_CALL hooks that will normally be a no-op. When optimized it should be zero cost and just leverage safepoint.

commit b6b8beecf77e39e75c7bb7cf998f4bf7e51311c7
Author: Charles Oliver Nutter <headius@headius.com>
Date:   Tue Feb 21 09:59:18 2023 -0600

    WIP c_call c_return

diff --git a/core/src/main/java/org/jruby/internal/runtime/methods/InvocationMethodFactory.java b/core/src/main/java/org/jruby/internal/runtime/methods/InvocationMethodFactory.java
index 06d4313503..bbb1c7fb45 100644
--- a/core/src/main/java/org/jruby/internal/runtime/methods/InvocationMethodFactory.java
+++ b/core/src/main/java/org/jruby/internal/runtime/methods/InvocationMethodFactory.java
@@ -253,10 +253,10 @@ public class InvocationMethodFactory extends MethodFactory implements Opcodes {
         }
 
         String generatedClassName = CodegenUtils.getAnnotatedBindingClassName(javaMethodName, desc1.declaringClassName, desc1.isStatic, desc1.actualRequired, desc1.optional, descs.size() > 1, desc1.anno.frame());
-        if (RubyInstanceConfig.FULL_TRACE_ENABLED) {
+//        if (RubyInstanceConfig.FULL_TRACE_ENABLED) {
             // in debug mode we append _DBG to class name to force it to regenerate (or use pre-generated debug version)
             generatedClassName += "_DBG";
-        }
+//        }
         String generatedClassPath = generatedClassName.replace('.', '/');
 
         DescriptorInfo info = new DescriptorInfo(descs);
@@ -738,7 +738,7 @@ public class InvocationMethodFactory extends MethodFactory implements Opcodes {
             invokeCallConfigPre(method, superClass, specificArity, block, callConfig);
         }
 
-        final boolean FULL_TRACE_ENABLED = RubyInstanceConfig.FULL_TRACE_ENABLED;
+        final boolean FULL_TRACE_ENABLED = true; //RubyInstanceConfig.FULL_TRACE_ENABLED;
 
         int traceBoolIndex = -1;
         if (FULL_TRACE_ENABLED) {
diff --git a/core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java b/core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java
index 670f195e8d..f06fb45f28 100644
--- a/core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java
+++ b/core/src/main/java/org/jruby/ir/runtime/IRRuntimeHelpers.java
@@ -2461,6 +2461,17 @@ public class IRRuntimeHelpers {
         }
     }
 
+    @JIT
+    public static void ccallTrace(ThreadContext context, IRubyObject self, RubyEvent event, String name, String filename, int line) {
+        TraceEventManager traceEvents = context.traceEvents;
+        if (traceEvents.hasEventHooks()) {
+            // FIXME: Try and statically generate END linenumber instead of hacking it.
+            int linenumber = line == -1 ? context.getLine() : line;
+
+            traceEvents.callEventHooks(context, event, filename, linenumber, name, self.getType());
+        }
+    }
+
     public static void traceRaise(ThreadContext context) {
         TraceEventManager traceEvents = context.traceEvents;
         if (traceEvents.hasEventHooks()) {
diff --git a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
index d20cf2c202..bc1f367837 100644
--- a/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
+++ b/core/src/main/java/org/jruby/ir/targets/JVMVisitor.java
@@ -2490,9 +2490,10 @@ public class JVMVisitor extends IRVisitor {
                 jvmMethod().loadFrameClass();
 
                 jvmAdapter().invokedynamic(
-                        name,
+                        "callTrace",
                         sig(void.class, ThreadContext.class, IRubyObject.class),
                         CallTraceSite.BOOTSTRAP,
+                        name,
                         traceInstr.getEvent().getName(),
                         traceInstr.getFilename(),
                         traceInstr.getLinenumber());
@@ -2504,9 +2505,10 @@ public class JVMVisitor extends IRVisitor {
                 jvmMethod().loadSelfBlock();
 
                 jvmAdapter().invokedynamic(
-                        name,
+                        "callTrace",
                         sig(void.class, ThreadContext.class, Block.class),
                         CallTraceSite.BOOTSTRAP,
+                        name,
                         traceInstr.getEvent().getName(),
                         traceInstr.getFilename(),
                         traceInstr.getLinenumber());
diff --git a/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java b/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java
index d4e1f061ba..b287113600 100644
--- a/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java
+++ b/core/src/main/java/org/jruby/ir/targets/indy/Bootstrap.java
@@ -67,6 +67,7 @@ import org.jruby.runtime.CompiledIRBlockBody;
 import org.jruby.runtime.DynamicScope;
 import org.jruby.runtime.Frame;
 import org.jruby.runtime.Helpers;
+import org.jruby.runtime.RubyEvent;
 import org.jruby.runtime.ThreadContext;
 import org.jruby.runtime.builtin.IRubyObject;
 import org.jruby.runtime.callsite.CacheEntry;
@@ -1022,7 +1023,7 @@ public class Bootstrap {
         return mh;
     }
 
-    static MethodHandle buildNativeHandle(InvokeSite site, CacheEntry entry, boolean blockGiven) {
+    static MethodHandle buildNativeHandle(InvokeSite site, CacheEntry entry, IRubyObject self, boolean blockGiven) {
         MethodHandle mh = null;
         SmartBinder binder = null;
         DynamicMethod method = entry.method;
@@ -1101,6 +1102,26 @@ public class Bootstrap {
                 if (anno != null && anno.frame()) {
                     mh = InvocationLinker.wrapWithFrameOnly(site.signature, entry.sourceModule, site.name(), mh);
                 }
+
+                SmartHandle cCallTrace = SmartBinder.from(site.signature)
+                        .permute("context", "self")
+                        .append("event", RubyEvent.C_CALL)
+                        .append(
+                                arrayOf("event", "name", "file", "line"),
+                                arrayOf(RubyEvent.class, String.class, String.class, int.class),
+                                RubyEvent.C_CALL, site.name(), site.file, site.line)
+                        .invoke(self.getRuntime().getTraceEvents().getCCallCReturnSite().dynamicInvoker());
+
+                mh = MethodHandles.foldArguments(mh, cCallTrace.handle());
+
+                SmartHandle cReturnTrace = SmartBinder.from(site.signature.prependArg("retval", IRubyObject.class))
+                        .permute("context", "self", "retval")
+                        .append("event", RubyEvent.C_RETURN)
+                        .append(
+                                arrayOf("event", "name", "file", "line"),
+                                arrayOf(RubyEvent.class, String.class, String.class, int.class),
+                                RubyEvent.C_CALL, site.name(), site.file, site.line)
+                        .invoke(self.getRuntime().getTraceEvents().getCCallCReturnSite().dynamicInvoker());
             }
         }
 
diff --git a/core/src/main/java/org/jruby/ir/targets/indy/CallTraceSite.java b/core/src/main/java/org/jruby/ir/targets/indy/CallTraceSite.java
index b329c14516..778e9046fe 100644
--- a/core/src/main/java/org/jruby/ir/targets/indy/CallTraceSite.java
+++ b/core/src/main/java/org/jruby/ir/targets/indy/CallTraceSite.java
@@ -39,10 +39,10 @@ public class CallTraceSite extends MutableCallSite {
     public static final Handle BOOTSTRAP = new Handle(
             Opcodes.H_INVOKESTATIC,
             p(CallTraceSite.class),
-            "bootstrap", sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, String.class, int.class),
+            "bootstrap", sig(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class, String.class, String.class, String.class, int.class),
             false);
 
-    public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type, String event, String file, int line) {
+    public static CallSite bootstrap(MethodHandles.Lookup lookup, String unused, MethodType type, String name, String event, String file, int line) {
         CallTraceSite site = new CallTraceSite(type, event, name, file, line);
         MethodHandle traceSetup = Binder
                         .from(type)
diff --git a/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java b/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java
index f941b08eb9..8dd7a86f25 100644
--- a/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java
+++ b/core/src/main/java/org/jruby/ir/targets/indy/InvokeSite.java
@@ -533,7 +533,7 @@ public abstract class InvokeSite extends MutableCallSite {
 
         MethodHandle mh = buildNewInstanceHandle(entry, self);
         if (mh == null) mh = buildNotEqualHandle(entry, self);
-        if (mh == null) mh = Bootstrap.buildNativeHandle(this, entry, blockGiven);
+        if (mh == null) mh = Bootstrap.buildNativeHandle(this, entry, self, blockGiven);
         if (mh == null) mh = buildJavaFieldHandle(this, entry, self);
         if (mh == null) mh = Bootstrap.buildIndyHandle(this, entry);
         if (mh == null) mh = Bootstrap.buildJittedHandle(this, entry, blockGiven);
diff --git a/core/src/main/java/org/jruby/runtime/TraceEventManager.java b/core/src/main/java/org/jruby/runtime/TraceEventManager.java
index c82a8ea0ec..9d13f5162f 100644
--- a/core/src/main/java/org/jruby/runtime/TraceEventManager.java
+++ b/core/src/main/java/org/jruby/runtime/TraceEventManager.java
@@ -28,6 +28,13 @@ public class TraceEventManager {
             .from(void.class, ThreadContext.class, IRubyObject.class, RubyEvent.class, String.class, String.class, int.class)
             .drop(1, 5)
             .identity();
+    public static final MethodHandle C_TRACE_ON = Binder
+            .from(void.class, ThreadContext.class, IRubyObject.class, RubyEvent.class, String.class, String.class, int.class)
+            .invokeStaticQuiet(LOOKUP, IRRuntimeHelpers.class, "ccallTrace");
+    public static final MethodHandle C_TRACE_OFF = Binder
+            .from(void.class, ThreadContext.class, IRubyObject.class, RubyEvent.class, String.class, String.class, int.class)
+            .drop(1, 5)
+            .identity();
     public static final MethodHandle B_TRACE_ON = Binder
             .from(void.class, ThreadContext.class, Block.class, RubyEvent.class, String.class, String.class, int.class)
             .invokeStaticQuiet(LOOKUP, IRRuntimeHelpers.class, "callTrace");
@@ -44,6 +51,7 @@ public class TraceEventManager {
     private final CallTraceFuncHook callTraceFuncHook = new CallTraceFuncHook(null);
 
     private final MutableCallSite callTrace = new MutableCallSite(TRACE_OFF);
+    private final MutableCallSite ccallTrace = new MutableCallSite(C_TRACE_OFF);
     private final MutableCallSite bcallTrace = new MutableCallSite(B_TRACE_OFF);
 
     public TraceEventManager(Ruby runtime) {
@@ -112,6 +120,10 @@ public class TraceEventManager {
             callTrace.setTarget(TRACE_ON);
         }
 
+        if (hook.isInterestedInEvent(RubyEvent.C_CALL) || hook.isInterestedInEvent(RubyEvent.C_RETURN)) {
+            ccallTrace.setTarget(C_TRACE_ON);
+        }
+
         if (hook.isInterestedInEvent(RubyEvent.B_CALL) || hook.isInterestedInEvent(RubyEvent.B_RETURN)) {
             bcallTrace.setTarget(B_TRACE_ON);
         }
@@ -122,6 +134,10 @@ public class TraceEventManager {
             callTrace.setTarget(TRACE_OFF);
         }
 
+        if (hook.isInterestedInEvent(RubyEvent.C_CALL) || hook.isInterestedInEvent(RubyEvent.C_RETURN)) {
+            ccallTrace.setTarget(C_TRACE_OFF);
+        }
+
         if (hook.isInterestedInEvent(RubyEvent.B_CALL) || hook.isInterestedInEvent(RubyEvent.B_RETURN)) {
             bcallTrace.setTarget(B_TRACE_OFF);
         }
@@ -193,6 +209,10 @@ public class TraceEventManager {
         return callTrace;
     }
 
+    public MutableCallSite getCCallCReturnSite() {
+        return callTrace;
+    }
+
     public MutableCallSite getBCallBReturnSite() {
         return bcallTrace;
     }

See jruby#7673 for a discussion of why we are opting not to enable
always-on call and return events for now.
@headius headius marked this pull request as ready for review March 14, 2023 20:23
@headius headius merged commit 0601c82 into jruby:master Mar 14, 2023
@headius headius deleted the always_on_traces branch March 14, 2023 20:24
@headius headius changed the title Always on traces Enable more traces and reduce costs of others Mar 14, 2023
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.

2 participants