1 /*
2  * Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 
25 package org.graalvm.compiler.core;
26 
27 import static org.graalvm.compiler.core.CompilationWrapper.ExceptionAction.ExitVM;
28 import static org.graalvm.compiler.core.GraalCompilerOptions.CompilationBailoutAsFailure;
29 import static org.graalvm.compiler.core.GraalCompilerOptions.CompilationFailureAction;
30 import static org.graalvm.compiler.core.GraalCompilerOptions.ExitVMOnException;
31 import static org.graalvm.compiler.core.GraalCompilerOptions.MaxCompilationProblemsPerAction;
32 import static org.graalvm.compiler.core.common.GraalOptions.TrackNodeSourcePosition;
33 import static org.graalvm.compiler.debug.DebugOptions.Dump;
34 import static org.graalvm.compiler.debug.DebugOptions.DumpPath;
35 import static org.graalvm.compiler.debug.DebugOptions.MethodFilter;
36 
37 import java.io.ByteArrayOutputStream;
38 import java.io.File;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.PrintStream;
42 import java.util.Map;
43 
44 import org.graalvm.compiler.debug.DebugContext;
45 import org.graalvm.compiler.debug.DebugOptions;
46 import org.graalvm.compiler.debug.DiagnosticsOutputDirectory;
47 import org.graalvm.compiler.debug.PathUtilities;
48 import org.graalvm.compiler.debug.TTY;
49 import org.graalvm.compiler.options.OptionValues;
50 
51 import jdk.vm.ci.code.BailoutException;
52 
53 /**
54  * Wrapper for a compilation that centralizes what action to take based on
55  * {@link GraalCompilerOptions#CompilationBailoutAsFailure} and
56  * {@link GraalCompilerOptions#CompilationFailureAction} when an uncaught exception occurs during
57  * compilation.
58  */
59 public abstract class CompilationWrapper<T> {
60 
61     /**
62      * Actions to take upon an exception being raised during compilation performed via
63      * {@link CompilationWrapper}. The actions are with respect to what the user sees on the
64      * console. The compilation requester determines what ultimate action is taken in
65      * {@link CompilationWrapper#handleException(Throwable)}.
66      *
67      * The actions are in ascending order of verbosity.
68      */
69     public enum ExceptionAction {
70         /**
71          * Print nothing to the console.
72          */
73         Silent,
74 
75         /**
76          * Print a stack trace to the console.
77          */
78         Print,
79 
80         /**
81          * An exception causes the compilation to be retried with extra diagnostics enabled.
82          */
83         Diagnose,
84 
85         /**
86          * Same as {@link #Diagnose} except that the VM process is exited after retrying.
87          */
88         ExitVM;
89 
90         private static final ExceptionAction[] VALUES = values();
91 
92         /**
93          * Gets the action that is one level less verbose than this action, bottoming out at the
94          * least verbose action.
95          */
quieter()96         ExceptionAction quieter() {
97             assert ExceptionAction.Silent.ordinal() == 0;
98             int index = Math.max(ordinal() - 1, 0);
99             return VALUES[index];
100         }
101     }
102 
103     private final DiagnosticsOutputDirectory outputDirectory;
104 
105     private final Map<ExceptionAction, Integer> problemsHandledPerAction;
106 
107     /**
108      * @param outputDirectory object used to access a directory for dumping if the compilation is
109      *            re-executed
110      * @param problemsHandledPerAction map used to count the number of compilation failures or
111      *            bailouts handled by each action. This is provided by the caller as it is expected
112      *            to be shared between instances of {@link CompilationWrapper}.
113      */
CompilationWrapper(DiagnosticsOutputDirectory outputDirectory, Map<ExceptionAction, Integer> problemsHandledPerAction)114     public CompilationWrapper(DiagnosticsOutputDirectory outputDirectory, Map<ExceptionAction, Integer> problemsHandledPerAction) {
115         this.outputDirectory = outputDirectory;
116         this.problemsHandledPerAction = problemsHandledPerAction;
117     }
118 
119     /**
120      * Handles an uncaught exception.
121      *
122      * @param t an exception thrown during {@link #run(DebugContext)}
123      * @return a value representing the result of a failed compilation (may be {@code null})
124      */
handleException(Throwable t)125     protected abstract T handleException(Throwable t);
126 
127     /**
128      * Gets the action to take based on the value of
129      * {@link GraalCompilerOptions#CompilationBailoutAsFailure},
130      * {@link GraalCompilerOptions#CompilationFailureAction} and
131      * {@link GraalCompilerOptions#ExitVMOnException} in {@code options}.
132      *
133      * Subclasses can override this to choose a different action.
134      *
135      * @param cause the cause of the bailout or failure
136      */
lookupAction(OptionValues options, Throwable cause)137     protected ExceptionAction lookupAction(OptionValues options, Throwable cause) {
138         if (cause instanceof BailoutException && !CompilationBailoutAsFailure.getValue(options)) {
139             return ExceptionAction.Silent;
140         }
141         if (ExitVMOnException.getValue(options)) {
142             assert CompilationFailureAction.getDefaultValue() != ExceptionAction.ExitVM;
143             assert ExitVMOnException.getDefaultValue() != true;
144             if (CompilationFailureAction.hasBeenSet(options) && CompilationFailureAction.getValue(options) != ExceptionAction.ExitVM) {
145                 TTY.printf("WARNING: Ignoring %s=%s since %s=true has been explicitly specified.%n",
146                                 CompilationFailureAction.getName(), CompilationFailureAction.getValue(options),
147                                 ExitVMOnException.getName());
148             }
149             return ExceptionAction.ExitVM;
150         }
151         return CompilationFailureAction.getValue(options);
152     }
153 
154     /**
155      * Perform the compilation wrapped by this object.
156      *
157      * @param debug the debug context to use for the compilation
158      */
performCompilation(DebugContext debug)159     protected abstract T performCompilation(DebugContext debug);
160 
161     /**
162      * Gets a value that represents the input to the compilation.
163      */
164     @Override
toString()165     public abstract String toString();
166 
167     /**
168      * Creates the {@link DebugContext} to use when retrying a compilation.
169      *
170      * @param initialDebug the debug context used in the failing compilation
171      * @param options the options for configuring the debug context
172      * @param logStream the log stream to use in the debug context
173      */
createRetryDebugContext(DebugContext initialDebug, OptionValues options, PrintStream logStream)174     protected abstract DebugContext createRetryDebugContext(DebugContext initialDebug, OptionValues options, PrintStream logStream);
175 
176     @SuppressWarnings("try")
run(DebugContext initialDebug)177     public final T run(DebugContext initialDebug) {
178         try {
179             return performCompilation(initialDebug);
180         } catch (Throwable cause) {
181             OptionValues initialOptions = initialDebug.getOptions();
182 
183             synchronized (CompilationFailureAction) {
184                 // Serialize all compilation failure handling.
185                 // This prevents retry compilation storms and interleaving
186                 // of compilation exception messages.
187                 // It also allows for reliable testing of CompilationWrapper
188                 // by avoiding a race whereby retry compilation output from a
189                 // forced crash (i.e., use of GraalCompilerOptions.CrashAt)
190                 // is truncated.
191 
192                 ExceptionAction action = lookupAction(initialOptions, cause);
193 
194                 action = adjustAction(initialOptions, action);
195 
196                 if (action == ExceptionAction.Silent) {
197                     return handleException(cause);
198                 }
199 
200                 if (action == ExceptionAction.Print) {
201                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
202                     try (PrintStream ps = new PrintStream(baos)) {
203                         ps.printf("%s: Compilation of %s failed: ", Thread.currentThread(), this);
204                         cause.printStackTrace(ps);
205                         ps.printf("To disable compilation failure notifications, set %s to %s (e.g., -Dgraal.%s=%s).%n",
206                                         CompilationFailureAction.getName(), ExceptionAction.Silent,
207                                         CompilationFailureAction.getName(), ExceptionAction.Silent);
208                         ps.printf("To capture more information for diagnosing or reporting a compilation failure, " +
209                                         "set %s to %s or %s (e.g., -Dgraal.%s=%s).%n",
210                                         CompilationFailureAction.getName(), ExceptionAction.Diagnose,
211                                         ExceptionAction.ExitVM,
212                                         CompilationFailureAction.getName(), ExceptionAction.Diagnose);
213                     }
214                     TTY.print(baos.toString());
215                     return handleException(cause);
216                 }
217 
218                 // action is Diagnose or ExitVM
219 
220                 if (Dump.hasBeenSet(initialOptions)) {
221                     // If dumping is explicitly enabled, Graal is being debugged
222                     // so don't interfere with what the user is expecting to see.
223                     return handleException(cause);
224                 }
225 
226                 File dumpPath = null;
227                 try {
228                     String dir = this.outputDirectory.getPath();
229                     if (dir != null) {
230                         String dumpName = PathUtilities.sanitizeFileName(toString());
231                         dumpPath = new File(dir, dumpName);
232                         dumpPath.mkdirs();
233                         if (!dumpPath.exists()) {
234                             TTY.println("Warning: could not create diagnostics directory " + dumpPath);
235                             dumpPath = null;
236                         }
237                     }
238                 } catch (Throwable t) {
239                     TTY.println("Warning: could not create Graal diagnostic directory");
240                     t.printStackTrace(TTY.out);
241                 }
242 
243                 String message;
244                 ByteArrayOutputStream baos = new ByteArrayOutputStream();
245                 try (PrintStream ps = new PrintStream(baos)) {
246                     // This output is used by external tools to detect compilation failures.
247                     ps.println("[[[Graal compilation failure]]]");
248 
249                     ps.printf("%s: Compilation of %s failed:%n", Thread.currentThread(), this);
250                     cause.printStackTrace(ps);
251                     ps.printf("To disable compilation failure notifications, set %s to %s (e.g., -Dgraal.%s=%s).%n",
252                                     CompilationFailureAction.getName(), ExceptionAction.Silent,
253                                     CompilationFailureAction.getName(), ExceptionAction.Silent);
254                     ps.printf("To print a message for a compilation failure without retrying the compilation, " +
255                                     "set %s to %s (e.g., -Dgraal.%s=%s).%n",
256                                     CompilationFailureAction.getName(), ExceptionAction.Print,
257                                     CompilationFailureAction.getName(), ExceptionAction.Print);
258                     if (dumpPath != null) {
259                         ps.println("Retrying compilation of " + this);
260                     } else {
261                         ps.println("Not retrying compilation of " + this + " as the dump path could not be created.");
262                     }
263                     message = baos.toString();
264                 }
265 
266                 TTY.print(message);
267                 if (dumpPath == null) {
268                     return handleException(cause);
269                 }
270 
271                 File retryLogFile = new File(dumpPath, "retry.log");
272                 try (PrintStream ps = new PrintStream(new FileOutputStream(retryLogFile))) {
273                     ps.print(message);
274                 } catch (IOException ioe) {
275                     TTY.printf("Error writing to %s: %s%n", retryLogFile, ioe);
276                 }
277 
278                 OptionValues retryOptions = new OptionValues(initialOptions,
279                                 Dump, ":" + DebugOptions.DiagnoseDumpLevel.getValue(initialOptions),
280                                 MethodFilter, null,
281                                 DumpPath, dumpPath.getPath(),
282                                 TrackNodeSourcePosition, true);
283 
284                 ByteArrayOutputStream logBaos = new ByteArrayOutputStream();
285                 PrintStream ps = new PrintStream(logBaos);
286                 try (DebugContext retryDebug = createRetryDebugContext(initialDebug, retryOptions, ps)) {
287                     T res = performCompilation(retryDebug);
288                     ps.println("There was no exception during retry.");
289                     maybeExitVM(action);
290                     return res;
291                 } catch (Throwable e) {
292                     ps.println("Exception during retry:");
293                     e.printStackTrace(ps);
294                     // Failures during retry are silent
295                     T res = handleException(cause);
296                     maybeExitVM(action);
297                     return res;
298                 } finally {
299                     ps.close();
300                     try (FileOutputStream fos = new FileOutputStream(retryLogFile, true)) {
301                         fos.write(logBaos.toByteArray());
302                     } catch (Throwable e) {
303                         TTY.printf("Error writing to %s: %s%n", retryLogFile, e);
304                     }
305                 }
306             }
307         }
308     }
309 
310     /**
311      * Calls {@link System#exit(int)} in the runtime embedding the Graal compiler. This will be a
312      * different runtime than Graal's runtime in the case of libgraal.
313      */
exitHostVM(int status)314     protected abstract void exitHostVM(int status);
315 
maybeExitVM(ExceptionAction action)316     private void maybeExitVM(ExceptionAction action) {
317         if (action == ExitVM) {
318             TTY.println("Exiting VM after retry compilation of " + this);
319             exitHostVM(-1);
320         }
321     }
322 
323     /**
324      * Adjusts {@code initialAction} if necessary based on
325      * {@link GraalCompilerOptions#MaxCompilationProblemsPerAction}.
326      */
adjustAction(OptionValues initialOptions, ExceptionAction initialAction)327     private ExceptionAction adjustAction(OptionValues initialOptions, ExceptionAction initialAction) {
328         ExceptionAction action = initialAction;
329         int maxProblems = MaxCompilationProblemsPerAction.getValue(initialOptions);
330         if (action != ExceptionAction.ExitVM) {
331             synchronized (problemsHandledPerAction) {
332                 while (action != ExceptionAction.Silent) {
333                     int problems = problemsHandledPerAction.getOrDefault(action, 0);
334                     if (problems >= maxProblems) {
335                         if (problems == maxProblems) {
336                             TTY.printf("Warning: adjusting %s from %s to %s after %s (%d) failed compilations%n", CompilationFailureAction, action, action.quieter(),
337                                             MaxCompilationProblemsPerAction, maxProblems);
338                             // Ensure that the message above is only printed once
339                             problemsHandledPerAction.put(action, problems + 1);
340                         }
341                         action = action.quieter();
342                     } else {
343                         break;
344                     }
345                 }
346                 problemsHandledPerAction.put(action, problemsHandledPerAction.getOrDefault(action, 0) + 1);
347             }
348         }
349         return action;
350     }
351 }
352