Closed Bug 274467 Opened 20 years ago Closed 20 years ago

Add JavaScript stack trace to exceptions

Categories

(Rhino Graveyard :: Core, enhancement)

1.6R1
x86
Windows 2000
enhancement
Not set
normal

Tracking

(Not tracked)

RESOLVED FIXED

People

(Reporter: djgredler, Assigned: igor)

Details

Attachments

(6 files, 8 obsolete files)

1.35 KB, text/plain
Details
2.53 KB, text/plain
Details
21.09 KB, patch
Details | Diff | Splinter Review
2.11 KB, text/plain
Details
2.04 KB, patch
Details | Diff | Splinter Review
1.36 KB, patch
Details | Diff | Splinter Review
It would be nice to have a JavaScript stack trace in exceptions thrown by Rhino, maybe in EcmaError. Right now EcmaError has sourceName, lineNumber, columnNumber and lineSource, but it would be nice to have this data for each call on the stack at the time of the error. This would make it much easier for applications that embed Rhino to debug their problems.
Do you use the default compile-to-jvm-classes mode in Rhino? In that case the standard Java printStackTrace should provide information about error locations in script. On the other hand if you use pure interpretation (optimization level == -1), then Java printStackTrace is useless indeed.
Well, our production system is using Rhino in interpreted mode, since it relies on continuations. A detailed stack trace would be nice indeed. I understand it might be hard to generate, as several invocations of Interpreter.interpret() can be on the Java stack, interweaved with calls to Java methods on host objects that call back into interpreted JavaScript functions, so it's possible this call stack should be maintained in a thread local. Now, instad of updating it on each method call, an optimization would be to actually only maintain a stack of top frames for Interpreter.interpret() invocations when they transition into Java code - the top frame in Interpreter().interpret() call would get pushed on it prior to calling a native Java function, and popped from it upon return. On exception, the current interpret() would first generate a trace from its own frames walking the parentFrame chain starting from its top frame, and then do the same for all the other top frames found on the thread local stack.
Just a heads-up, I've started working on this; should be done soon. I think I found an elegant way to embed the functionality.
Place it into a package named "test"
Attached file Output of the test program (obsolete) —
You can see two JavaScript stack trace elements: ... at script(bar:3) ... at script(foo:2) ... belonging to two nested invocations of the interpreter interleaved with Java stack trace elements.
To observations about the patch: 1. It introduces the dependency on JDK 1.4 via direct call to Throawable.initCause(). Please avoid it. 2. It makes constructions of RhinoException instances significantly heavier even when users use class compiler. I think storing just reference to CallFrameHolder during construction should be enough to print the stack trace if necessary.
(In reply to comment #7) > To observations about the patch: > > 1. It introduces the dependency on JDK 1.4 via direct call to > Throawable.initCause(). Please avoid it. > I had to add it so the stack trace is recreated in order for the causes to be reflected in the annotated stack trace as well. I further inspected where is it called from, and I can get around it by making captureJavaScriptStackTrace() package-private, and make sure it's called from Kit.initCause when it is invoked with a RhinoException. > 2. It makes constructions of RhinoException instances significantly heavier even > when users use class compiler. I think storing just reference to CallFrameHolder > during construction should be enough to print the stack trace if necessary. My only problem with that is that the locations CallFrame objects point (and actually the CallFrame objects referred to in the CallFrameHolder) at the time the exception is printed can be significantly different than when the exception was created. As a remedy for class compiler case, the captureJavaScriptStackTrace() could check for cx.interpreterLineCounting == null and shortcut the whole dump-to-string-writer business. Hope it'll be acceptable with these changes.
(In reply to comment #8) > > 2. It makes constructions of RhinoException instances significantly heavier > even > > when users use class compiler. I think storing just reference to > CallFrameHolder > > during construction should be enough to print the stack trace if necessary. > > My only problem with that is that the locations CallFrame objects point (and > actually the CallFrame objects referred to in the CallFrameHolder) at the time > the exception is printed can be significantly different than when the exception > was created. But then both holder and frame can be stored or the holder can be cloned to get a frozen copy. Not all exceptions get their stack trace printed and it does not sound right to execute all the code to produce a string with nicely formated trace. > > Hope it'll be acceptable with these changes. Sure.
Okay, I think I understood the concept of freezing call frames and call frame holders objects and cloning them on-demand. Here's a new version that should keep the lightweight construction of the exception - only the frame holder is stored, with all holders and frames reachable from it frozen. I'll attach a new testcase where the trace is printed from an entirely different interpreter session, and it works nicely. I even managed to make the printing process as lightweight as possible, by implementing a special filtering PrintStream and PrintWriter for the purpose, so no buffering/reparsing of the printed stack trace is required.
Attachment #173850 - Attachment is obsolete: true
Attachment #173851 - Attachment is obsolete: true
Attachment #173853 - Attachment is obsolete: true
(In reply to comment #10) > Created an attachment (id=173902) [edit] > Another take, now with freezing frames > > Okay, I think I understood the concept of freezing call frames and call frame > holders objects and cloning them on-demand. Here's a new version that should > keep the lightweight construction of the exception - only the frame holder is > stored, with all holders and frames reachable from it frozen. You do not need to freeze frames! Your code to print stack trace just uses idata and parentFrame, but that would not change during the following calculations in the Interpreter.interpret. In fact, to print stack trace you just need the heads of CallFrame chains from each Interpreter invocation on Java stack. So it seems the exception constructor can calculate the depth of CallFrameHolder chain, allocate CallFrame array, fill it with heads of CallFrame chains and store that in the exception.
I improved the last night's version slightly. There are no longer two separate classes for printing the stack trace (one PrintStream subclass and one PrintWriter subclass) with nearly identical code. I kept only the PrintWriter one, and when printing to a PrintStream is required, I wrap it in an output stream writer and print writer first. I really wish Sun engineered PrintStream and PrintWriter so that they implement a common interface declaring all of their print...() method. Actually, I'm not the only one to wish for this as witnessed on: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5035587
Attachment #173902 - Attachment is obsolete: true
(In reply to own comment #13) > (In reply to comment #10) > > Created an attachment (id=173902) [edit] [edit] > > Another take, now with freezing frames > > > > Okay, I think I understood the concept of freezing call frames and call frame > > holders objects and cloning them on-demand. Here's a new version that should > > keep the lightweight construction of the exception - only the frame holder is > > stored, with all holders and frames reachable from it frozen. > > You do not need to freeze frames! Your code to print stack trace just uses idata > and parentFrame, That is, of cause, wrong, since the code uses pcSourceLineStart which would change. So with the above it is necessary to capture array of lines per each InterpreterFarme that with CallFrame linked list would provide stack trace information. Now that does not sound particularly better then frame freezing since the later would avoid allocating of any objects if there are no exception handlers in the Interpreter.interpret. On the other hand the exception origin information may only change if the exception is caught in Interpreter.interpret or bellow. If the catch handler belongs to the Java code, then its execution would not change the interpreter data in all practical cases. And if it is Interpreter.interpreter that prepares to execute catch code in JS, then it can explicitly force construction of stack information. Thus the idea would be in the exception constructor to store only the current CallFrameHolder. Then the exception should contain a method to force creation of stack trace. Interpreter would call the method when preparing to execute the catch handler in the script. Rhino embedding may also call this method if it catches RhinoException and plans to do something smart with it besides printing stack-related information immediately.
(In reply to comment #15) > > On the other hand the exception origin information may only change if the > exception is caught in Interpreter.interpret or bellow. If the catch handler > belongs to the Java code, then its execution would not change the interpreter > data in all practical cases. And if it is Interpreter.interpreter that prepares > to execute catch code in JS, then it can explicitly force construction of stack > information. > > Thus the idea would be in the exception constructor to store only the current > CallFrameHolder. Then the exception should contain a method to force creation of > stack trace. Interpreter would call the method when preparing to execute the > catch handler in the script. Rhino embedding may also call this method if it > catches RhinoException and plans to do something smart with it besides printing > stack-related information immediately. Well, I'm not seeing this either as being particularly better than frame freezing. There's again the issue of an exception stack trace being generated forcibly even if it might happen that it will never be printed. Also, it exposes an implementation detail to an "advanced" embedder - he must remember to call the generator method. Granted, this can only be an issue if the embedder happens to call an interpreted function/script from the catch block. Frame freezing code takes care of all corner cases in a self-contained way, and I don't think there's a big cost difference between taking a snapshot of locations for the stack trace in the interpreter catch handler vs. making clones of frames in the interpreter if it recovers from the exception. Especially since the code for management of freezing is already in place, and doing a forcible capture would require RhinoException to cache a string containing the printed trace. Therefore, frame freezing looks like a more elegant solution to me, but then I'm a rather new to the Rhino code and I accept that you have more insight into it.
(In reply to comment #16) > Well, I'm not seeing this either as being particularly better than frame > freezing. Frame freezing would lead to frame cloning if the exception is caught inside Interpreter.interpret and this is not exactly cheap operation. With a few nested script frames on top of try /catch in JS generating the stack may be cheaper then cloning the frames. > There's again the issue of an exception stack trace being generated > forcibly even if it might happen that it will never be printed. > Also, it exposes > an implementation detail to an "advanced" embedder - he must remember to call > the generator method. Granted, this can only be an issue if the embedder happens > to call an interpreted function/script from the catch block. That would not affect the capturing information. The only case when it would matter is when the handler would return somehow the exeception object back to the script before printing the stack trace. But that require to wrap execption before sending it to JS and Rhino can call the method itself in the wrapping code. Since code to generate the catch object for native exception would go through the same path, it would add that forcefull stack creation only in one place. > Frame freezing code > takes care of all corner cases in a self-contained way. Right, but that requires a lot more code then the original patch and that does not seems justifiable. > and I don't think > there's a big cost difference between taking a snapshot of locations for the > stack trace in the interpreter catch handler vs. making clones of frames in the > interpreter if it recovers from the exception. Especially since the code for > management of freezing is already in place, and doing a forcible capture would > require RhinoException to cache a string containing the printed trace. > Therefore, frame freezing looks like a more elegant solution to me, but then I'm > a rather new to the Rhino code and I accept that you have more insight into it.
Okay, here's another try - no frame freezing, RhinoException.generateStackTrace() called when a frame was found that can handle the exception, just before StateLoop is continued. generateStackTrace() also called in printStackTrace() (does nothing if it was already called before) so it handles the case where the exception bubbles up into Java code where the trace is printed without it ever being handled at all in the interpreter. How does it look now?
Attachment #173940 - Attachment is obsolete: true
Comment on attachment 173956 [details] [diff] [review] Yet another try without frame freezing > > for (;;) { > if (exState != EX_NO_JS_STATE) { > boolean onlyFinally = (exState != EX_CATCH_STATE); > indexReg = getExceptionHandler(frame, onlyFinally); > if (indexReg >= 0) { >+ if(throwable instanceof RhinoException) { >+ ((RhinoException)throwable).generateStackTrace(); >+ } > // We caught an exception, restart the loop > // with exception pending the processing at the loop > // start > continue StateLoop; > } That is not right as it would force stack trace for finally as well and would miss the case of class compiler between interpreter frames. I suggest to put the check to WrapFactory before calls to WrapFactory.wrapAsJavaObject. Otherwise the patch is OK.
I just wanted to send a big "Thank You" to Attila for working on this! Much appreciated!
(In reply to comment #20) > I just wanted to send a big "Thank You" to Attila for working on this! Much > appreciated! Well, I thank you for the appreciation. However, I've come under a rather heavy load on my pay job, so I'm not sure if I can keep working on it. If it really only requires a finishing touch or two, maybe Igor can take it over. Lacking this, I might be able to return to it in, like, two weeks.
Changes from the previous version: 1. In the new version RhinoException constructor duplicates the information that can change during the following execution. Then the captured immune data are used to augment the native stack trace with script information when requested. In particular the patch stores in RhinoException the array of top CallFrame instances from each interpreter invocation and array of CallFrame.pcSourceLineStart from all CallFrame instances from all interpreter invocations. It is relatively lightweight and should not impose noticable performace penalty if stack is not printed. 2. I replaced CallFrameHolder by a stack of top frames from each interpreter invocation. In this way there is no need to create an additional object per each interpreter invocation and creation of array of top frames is simplified. 3. The previous version depended on the fact that JVM would call PrintWriter.println() to print stack entries. Since there is no any guaranty that it would stay that way on any JVM where Rhino could run, I changed that to explicit parsing of the stack trace. To make life even I renamed the interpreter loop routine to interpreterLoop to have a simple tag to look for.
Attachment #173956 - Attachment is obsolete: true
1. Stack trace includes function name when available. 2. Patch tries to decrease the number of various Interpreter.interpret calls before the real interpretation can start. It makes the interpreter initialization slightly faster and stack trace looks more clear.
Attachment #174490 - Attachment is obsolete: true
I committed the last patch.
Status: NEW → RESOLVED
Closed: 20 years ago
Resolution: --- → FIXED
Recently we had a problem with a NPE occurring in a Java method - it wasn't wrapped into WrappedException and subsequently we did not see the interpreter script stack traces. This is because unchecked throwables aren't wrapped into InvocationTargetException. I changed the relevant code in MemberBox.java to wrap all exceptions (checked and unchecked) into WrappedException. BTW, this is now consistent with how JavaMembers.get() and JavaMembers.put() work - they also wrap all exceptions into WrappedException, not just InvocationTargetException.
Sorry, uploaded a wrong file previously
Attachment #175806 - Attachment is obsolete: true
Hm... For some reason, before I added the patch Bugzilla showed me an option to reopen the bug, now it doesn't.
I committed the last patch.
The wrapped exception carrying the embedded script stack trace can become inaccessible to the calling Java code if the script catches and rethrows exceptions. I.e. ---------------------------------------------------- try { new java.io.FileInputStream("nonexistant file"); } catch(e) { throw e; } ---------------------------------------------------- The Java code invoking the interpreter won't be able to retrieve the WrappedException that carries the script stack trace. It can only retrieve the underlying e.javaException using code similar to this: ---------------------------------------------------- Throwable terminationCause; try { script.exec(cx, scope); } catch(JavaScriptException e) { terminationCause = e; Object val = e.getValue(); if(val instanceof Scriptable) { Object njo = ScriptableObject.getProperty(((Scriptable)val), "javaException"); if(njo instanceof NativeJavaObject) { val = njo; } if(val instanceof NativeJavaObject) { Object t = ((NativeJavaObject)val).unwrap(); while(t instanceof InvocationTargetException) { t = ((InvocationTargetException)t).getCause(); } if(t instanceof Throwable) { terminationCause = (Throwable)t; } } } } catch(Throwable t) { terminationCause = t; } ---------------------------------------------------- I'm attaching a patch that'd add another property to the JavaScript error object, named "rhinoException" that'd contain the originating RhinoException. In my above code that attempts to extract the underlying exception from the JavaScriptException, I could then obtain the RhinoException by calling ScriptableObject.getProperty with "rhinoException" instead of "javaException". That way, I could still properly log exceptions with script stack trace even when the script does a try-catch-rethrow.
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: