Skip to content

Commit

Permalink
Multiple adjustments to the Exception and raise logic.
Browse files Browse the repository at this point in the history
* Request backtrace from exception object before raise, so that
  pre-set backtrace or overridden #backtrace skip native trace
  gathering.
* Eliminate some redundant or unused backtrace-gathering methods.
* Fix Kernel#warn uplevel logic to use partial traces (Java 9).

The fixes here allow two additional ways of blunting the cost of
raising an exception (by eliminating the native stack trace):

* Call Exception#set_backtrace before raising
* Use an Exception subtype that overrides Exception#backtrace

This improves the performance of these scenarios to be roughly
equivalent to the three-arg form of Kernel#raise.

Fixes jruby#5605.
  • Loading branch information
headius committed Feb 12, 2019
1 parent 6ad0355 commit f64a3e0
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 182 deletions.
8 changes: 0 additions & 8 deletions core/src/main/java/org/jruby/NativeException.java
Expand Up @@ -84,14 +84,6 @@ public static RubyClass createClass(Ruby runtime, RubyClass baseClass) {
return exceptionClass;
}

@Override
public void prepareBacktrace(ThreadContext context) {
// if it's null, use cause's trace to build a raw stack trace
if (backtraceData == null) {
backtraceData = WALKER.walk(cause.getStackTrace(), stream -> TraceType.Gather.RAW.getBacktraceData(getRuntime().getCurrentContext(), stream));
}
}

@JRubyMethod
public final IRubyObject cause() {
return Java.getInstance(getRuntime(), getCause());
Expand Down
4 changes: 1 addition & 3 deletions core/src/main/java/org/jruby/Ruby.java
Expand Up @@ -99,7 +99,6 @@
import org.jruby.exceptions.MainExitException;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.JRubyPOSIXHandler;
import org.jruby.ext.LateLoadingLibrary;
import org.jruby.ext.coverage.CoverageData;
import org.jruby.ext.ffi.FFI;
import org.jruby.ext.fiber.ThreadFiber;
Expand Down Expand Up @@ -144,7 +143,6 @@
import org.jruby.runtime.invokedynamic.MethodNames;
import org.jruby.runtime.load.BasicLibraryService;
import org.jruby.runtime.load.CompiledScriptLoader;
import org.jruby.runtime.load.Library;
import org.jruby.runtime.load.LoadService;
import org.jruby.runtime.opto.Invalidator;
import org.jruby.runtime.opto.OptoFactory;
Expand Down Expand Up @@ -4314,7 +4312,7 @@ public RaiseException newStopIteration(IRubyObject result, String message) {
RubyException ex = RubyStopIteration.newInstance(context, result, message);

if (!RubyInstanceConfig.STOPITERATION_BACKTRACE) {
ex.forceBacktrace(disabledBacktrace());
ex.setBacktrace(disabledBacktrace());
}

return ex.toThrowable();
Expand Down
141 changes: 76 additions & 65 deletions core/src/main/java/org/jruby/RubyException.java
Expand Up @@ -68,18 +68,68 @@
@JRubyClass(name="Exception")
public class RubyException extends RubyObject {

private static class Backtrace {
private BacktraceData backtraceData;
private IRubyObject backtraceObject;
private IRubyObject backtraceLocations;

/**
* Get the Ruby-facing representation of this backtrace, or a previously-set backtrace object.
*
* @param runtime the current runtime
* @return the Ruby object for this backtrace
*/
public final IRubyObject getBacktraceObject(Ruby runtime) {
IRubyObject backtraceObject = this.backtraceObject;

if (backtraceObject != null) return backtraceObject;

if (backtraceData == null || backtraceData == BacktraceData.EMPTY) return runtime.getNil();

return this.backtraceObject = TraceType.generateMRIBacktrace(runtime, backtraceData.getBacktrace(runtime));
}

/**
* Set the Ruby-facing backtrace object for this backtrace.
*
* @param backtraceObject the object to return for future backtrace requests
*/
public final void setBacktraceObject(IRubyObject backtraceObject) {
this.backtraceObject = backtraceObject;
}

/**
* Get an array of backtrace location objects for this backtrace.
*
* @param context the current thread context
* @return the array of backtrace locations
*/
public IRubyObject getBacktraceLocations(ThreadContext context) {
if (backtraceLocations != null) return backtraceLocations;

if (backtraceData == null) {
backtraceLocations = context.nil;
} else {
Ruby runtime = context.runtime;
backtraceLocations = RubyThread.Location.newLocationArray(runtime, backtraceData.getBacktrace(runtime));
}

return backtraceLocations;
}
}

public static final int TRACE_HEAD = 8;
public static final int TRACE_TAIL = 4;
public static final int TRACE_MAX = RubyException.TRACE_HEAD + RubyException.TRACE_TAIL + 6;
public static final String[] FULL_MESSAGE_KEYS = {"highlight", "order"};
protected BacktraceData backtraceData;

private final Backtrace backtrace = new Backtrace();

IRubyObject message;
// We initialize this to UNDEF to know whether cause has been initialized (from ruby space we will just see nil
// but internally we want to know if there was a cause or it was set to nil explicitly).
IRubyObject cause = UNDEF;
private IRubyObject backtrace;
private RaiseException throwable;
private IRubyObject backtraceLocations;

protected RubyException(Ruby runtime, RubyClass rubyClass) {
super(runtime, rubyClass);
Expand Down Expand Up @@ -232,30 +282,19 @@ public IRubyObject set_backtrace(IRubyObject obj) {
return backtrace();
}

private void setBacktrace(IRubyObject obj) {
if (obj.isNil()) {
backtrace = null;
} else if (isArrayOfStrings(obj)) {
backtrace = obj;
public void setBacktrace(IRubyObject obj) {
if (obj.isNil() || isArrayOfStrings(obj)) {
backtrace.backtraceObject = obj;
} else if (obj instanceof RubyString) {
backtrace = RubyArray.newArray(getRuntime(), obj);
backtrace.backtraceObject = RubyArray.newArray(getRuntime(), obj);
} else {
throw getRuntime().newTypeError("backtrace must be Array of String");
}
}

@JRubyMethod(omit = true)
public IRubyObject backtrace_locations(ThreadContext context) {
if (backtraceLocations != null) return backtraceLocations;

if (backtraceData == null) {
backtraceLocations = context.nil;
} else {
Ruby runtime = context.runtime;
backtraceLocations = RubyThread.Location.newLocationArray(runtime, backtraceData.getBacktrace(runtime));
}

return backtraceLocations;
return backtrace.getBacktraceLocations(context);
}

@JRubyMethod(optional = 1)
Expand Down Expand Up @@ -367,68 +406,33 @@ public Object getCause() {
return cause == UNDEF ? null : cause;
}

public void setBacktraceData(BacktraceData backtraceData) {
this.backtraceData = backtraceData;
}

public BacktraceData getBacktraceData() {
return backtraceData;
}

public RubyStackTraceElement[] getBacktraceElements() {
if (backtraceData == null) {
if (backtrace.backtraceData == null) {
return RubyStackTraceElement.EMPTY_ARRAY;
}
return backtraceData.getBacktrace(getRuntime());
return backtrace.backtraceData.getBacktrace(getRuntime());
}

public void prepareBacktrace(ThreadContext context) {
// if it's null, build a backtrace
if (backtraceData == null) {
backtraceData = context.runtime.getInstanceConfig().getTraceType().getBacktrace(context);
}
}

/**
* Prepare an "integrated" backtrace that includes the normal Ruby trace plus non-filtered Java frames. Used by
* Java integration to show the Java frames for a JI-called method.
*
* @param context
* @param javaTrace
*/
public void prepareIntegratedBacktrace(ThreadContext context, StackTraceElement[] javaTrace) {
// if it's null, build a backtrace
if (backtraceData == null) {
backtraceData = context.runtime.getInstanceConfig().getTraceType().getIntegratedBacktrace(context, javaTrace);
}
}

public void forceBacktrace(IRubyObject backtrace) {
backtraceData = (backtrace != null && backtrace.isNil()) ? null : BacktraceData.EMPTY;
setBacktrace(backtrace);
public void captureBacktrace(ThreadContext context) {
backtrace.backtraceData = context.runtime.getInstanceConfig().getTraceType().getBacktrace(context);
}

public IRubyObject getBacktrace() {
if (backtrace == null) {
initBacktrace();
IRubyObject backtraceObject = backtrace.backtraceObject;

if (backtraceObject != null) {
return backtrace.backtraceObject;
}
return backtrace;
}

public void initBacktrace() {
Ruby runtime = getRuntime();
if (backtraceData == null) {
backtrace = runtime.getNil();
} else {
backtrace = TraceType.generateMRIBacktrace(runtime, backtraceData.getBacktrace(runtime));
}

return backtrace.getBacktraceObject(runtime);
}

@Override
public void copySpecialInstanceVariables(IRubyObject clone) {
RubyException exception = (RubyException)clone;
exception.backtraceData = backtraceData;
exception.backtrace = backtrace;
exception.backtrace.backtraceData = backtrace.backtraceData;
exception.message = message;
}

Expand Down Expand Up @@ -520,4 +524,11 @@ public List<String> getVariableNameList() {
return names;
}

@Deprecated
public void prepareIntegratedBacktrace(ThreadContext context, StackTraceElement[] javaTrace) {
// if it's null, build a backtrace
if (backtrace.backtraceData == null) {
backtrace.backtraceData = context.runtime.getInstanceConfig().getTraceType().getIntegratedBacktrace(context, javaTrace);
}
}
}
27 changes: 14 additions & 13 deletions core/src/main/java/org/jruby/RubyKernel.java
Expand Up @@ -50,6 +50,7 @@
import java.util.Map;
import java.util.Set;

import com.headius.backport9.stack.StackWalker;
import jnr.constants.platform.Errno;
import jnr.posix.POSIX;

Expand Down Expand Up @@ -850,17 +851,16 @@ public static IRubyObject raise(ThreadContext context, IRubyObject recv, IRubyOb

// semi extract_raise_opts :
IRubyObject cause = null;
if ( argc > 0 ) {
IRubyObject last = args[argc - 1];
if ( last instanceof RubyHash ) {
RubyHash opt = (RubyHash) last; RubySymbol key;
if ( ! opt.isEmpty() && ( opt.has_key_p( context, key = runtime.newSymbol("cause") ) == runtime.getTrue() ) ) {
cause = opt.delete(context, key, Block.NULL_BLOCK);
forceCause = true;
if ( opt.isEmpty() && --argc == 0 ) { // more opts will be passed along
throw runtime.newArgumentError("only cause is given with no arguments");
}
}
IRubyObject maybeOpts = ArgsUtil.getOptionsArg(runtime, args);
if (!maybeOpts.isNil()) {
argc--;
RubyHash opt = (RubyHash) maybeOpts;
cause = opt.delete(context, runtime.newSymbol("cause"));

if (!cause.isNil()) {
forceCause = true;

if (argc == 0) throw runtime.newArgumentError("only cause is given with no arguments");
}
}

Expand Down Expand Up @@ -894,7 +894,7 @@ public static IRubyObject raise(ThreadContext context, IRubyObject recv, IRubyOb
break;
default:
RubyException exception = convertToException(context, args[0], args[1]);
exception.forceBacktrace(args[2]);
exception.setBacktrace(args[2]);
raise = exception.toThrowable();
break;
}
Expand Down Expand Up @@ -1248,6 +1248,7 @@ static IRubyObject warn(ThreadContext context, IRubyObject recv, RubyString mess
}

public static final String[] WARN_VALID_KEYS = { "uplevel" };
private static final StackWalker WALKER = StackWalker.getInstance();

@JRubyMethod(module = true, rest = true, visibility = PRIVATE)
public static IRubyObject warn(ThreadContext context, IRubyObject recv, IRubyObject[] args) {
Expand All @@ -1271,7 +1272,7 @@ public static IRubyObject warn(ThreadContext context, IRubyObject recv, IRubyObj
int numberOfMessages = kwargs ? args.length - 1 : args.length;

if (kwargs) {
RubyStackTraceElement element = context.runtime.getInstanceConfig().getTraceType().getBacktraceElement(context, uplevel);
RubyStackTraceElement element = context.getSingleBacktrace(uplevel);

RubyString baseMessage = context.runtime.newString();
baseMessage.catString(element.getFileName() + ':' + element.getLineNumber() + ": warning: ");
Expand Down
49 changes: 7 additions & 42 deletions core/src/main/java/org/jruby/exceptions/RaiseException.java
Expand Up @@ -94,22 +94,10 @@ private void preRaise(ThreadContext context) {
preRaise(context, (IRubyObject) null);
}

private void preRaise(ThreadContext context, StackTraceElement[] javaTrace) {
context.runtime.incrementExceptionCount();
doSetLastError(context);
doCallEventHook(context);

if (RubyInstanceConfig.LOG_EXCEPTIONS) TraceType.logException(exception);

if (requiresBacktrace(context)) {
exception.prepareIntegratedBacktrace(context, javaTrace);
}
}

private boolean requiresBacktrace(ThreadContext context) {
// We can only omit backtraces of descendents of Standard error for 'foo rescue nil'
return context.exceptionRequiresBacktrace ||
! context.runtime.getStandardError().isInstance(exception) ||
!context.runtime.getStandardError().isInstance(exception) ||
context.runtime.isDebug();
}

Expand All @@ -120,16 +108,14 @@ private void preRaise(ThreadContext context, IRubyObject backtrace) {

if (RubyInstanceConfig.LOG_EXCEPTIONS) TraceType.logException(exception);

// We can only omit backtraces of descendents of Standard error for 'foo rescue nil'
if (requiresBacktrace(context)) {
if (backtrace == null) {
exception.prepareBacktrace(context);
} else {
exception.forceBacktrace(backtrace);
if ( backtrace.isNil() ) return;
}
backtrace = backtrace == null ? exception.callMethod("backtrace") : backtrace;

if (backtrace.isNil()) {
// No backtrace provided or overridden, capture at this point in stack
exception.captureBacktrace(context);
setStackTrace(RaiseException.javaTraceFromRubyTrace(exception.getBacktraceElements()));
} else {
// Backtrace provided
}
}

Expand Down Expand Up @@ -159,27 +145,6 @@ public static StackTraceElement[] javaTraceFromRubyTrace(RubyStackTraceElement[]
return newTrace;
}

@Deprecated
public static RaiseException createNativeRaiseException(Ruby runtime, Throwable cause) {
return createNativeRaiseException(runtime, cause, null);
}

@Deprecated
public static RaiseException createNativeRaiseException(Ruby runtime, Throwable cause, Member target) {
org.jruby.NativeException nativeException = new org.jruby.NativeException(runtime, runtime.getNativeException(), cause);

return new RaiseException(cause, nativeException);
}

@Deprecated
public RaiseException(Throwable cause, org.jruby.NativeException nativeException) {
super(nativeException.getMessageAsJavaString(), cause);
providedMessage = super.getMessage(); // cause.getClass().getId() + ": " + message
setException(nativeException);
preRaise(nativeException.getRuntime().getCurrentContext(), nativeException.getCause().getStackTrace());
setStackTrace(RaiseException.javaTraceFromRubyTrace(exception.getBacktraceElements()));
}

@Deprecated
public RaiseException(RubyException exception) {
this(exception.getMessageAsJavaString(), exception);
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/org/jruby/management/Runtime.java
Expand Up @@ -40,6 +40,7 @@
import org.jruby.RubyThread;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.backtrace.BacktraceData;
import org.jruby.runtime.backtrace.TraceType.Format;
import org.jruby.runtime.backtrace.TraceType.Gather;

Expand Down Expand Up @@ -108,7 +109,7 @@ private static void dumpThread(Ruby ruby, RubyThread th, Gather gather, PrintWri
ThreadContext tc = th.getContext();
if (tc != null) {
RubyException exc = new RubyException(ruby, ruby.getRuntimeError(), "thread dump");
exc.setBacktraceData(WALKER.walk(th.getNativeThread().getStackTrace(), stream -> gather.getBacktraceData(tc, stream)));
exc.toThrowable();
pw.println(Format.MRI.printBacktrace(exc, false));
} else {
pw.println(" [no longer alive]");
Expand Down

0 comments on commit f64a3e0

Please sign in to comment.