| /* |
| * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| package org.graalvm.compiler.hotspot.test; |
| |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import org.graalvm.compiler.core.common.LocationIdentity; |
| import org.graalvm.compiler.debug.Debug; |
| import org.graalvm.compiler.debug.Debug.Scope; |
| import org.graalvm.compiler.debug.DebugConfig; |
| import org.graalvm.compiler.debug.DebugConfigScope; |
| import org.graalvm.compiler.debug.DebugDumpScope; |
| import org.graalvm.compiler.debug.internal.DebugScope; |
| import org.graalvm.compiler.hotspot.GraalHotSpotVMConfig; |
| import org.graalvm.compiler.hotspot.nodes.G1ArrayRangePostWriteBarrier; |
| import org.graalvm.compiler.hotspot.nodes.G1ArrayRangePreWriteBarrier; |
| import org.graalvm.compiler.hotspot.nodes.G1PostWriteBarrier; |
| import org.graalvm.compiler.hotspot.nodes.G1PreWriteBarrier; |
| import org.graalvm.compiler.hotspot.nodes.SerialArrayRangeWriteBarrier; |
| import org.graalvm.compiler.hotspot.nodes.SerialWriteBarrier; |
| import org.graalvm.compiler.hotspot.phases.WriteBarrierAdditionPhase; |
| import org.graalvm.compiler.hotspot.phases.WriteBarrierVerificationPhase; |
| import org.graalvm.compiler.hotspot.replacements.arraycopy.UnsafeArrayCopyNode; |
| import org.graalvm.compiler.nodes.AbstractBeginNode; |
| import org.graalvm.compiler.nodes.AbstractMergeNode; |
| import org.graalvm.compiler.nodes.FieldLocationIdentity; |
| import org.graalvm.compiler.nodes.FixedNode; |
| import org.graalvm.compiler.nodes.FixedWithNextNode; |
| import org.graalvm.compiler.nodes.LoopBeginNode; |
| import org.graalvm.compiler.nodes.LoopExitNode; |
| import org.graalvm.compiler.nodes.StructuredGraph; |
| import org.graalvm.compiler.nodes.StructuredGraph.AllowAssumptions; |
| import org.graalvm.compiler.nodes.memory.WriteNode; |
| import org.graalvm.compiler.nodes.spi.LoweringTool; |
| import org.graalvm.compiler.phases.OptimisticOptimizations; |
| import org.graalvm.compiler.phases.common.CanonicalizerPhase; |
| import org.graalvm.compiler.phases.common.GuardLoweringPhase; |
| import org.graalvm.compiler.phases.common.LoopSafepointInsertionPhase; |
| import org.graalvm.compiler.phases.common.LoweringPhase; |
| import org.graalvm.compiler.phases.common.inlining.InliningPhase; |
| import org.graalvm.compiler.phases.graph.ReentrantNodeIterator; |
| import org.graalvm.compiler.phases.graph.ReentrantNodeIterator.NodeIteratorClosure; |
| import org.graalvm.compiler.phases.tiers.HighTierContext; |
| import org.graalvm.compiler.phases.tiers.MidTierContext; |
| |
| import jdk.vm.ci.meta.ResolvedJavaField; |
| |
| /** |
| * The following tests validate the write barrier verification phase. For every tested snippet, an |
| * array of write barrier indices and the total write barrier number are passed as parameters. The |
| * indices denote the barriers that will be manually removed. The write barrier verification phase |
| * runs after the write barrier removal and depending on the result an assertion might be generated. |
| * The tests anticipate the presence or not of an assertion generated by the verification phase. |
| */ |
| public class WriteBarrierVerificationTest extends HotSpotGraalCompilerTest { |
| |
| public static int barrierIndex; |
| |
| private final GraalHotSpotVMConfig config = runtime().getVMConfig(); |
| |
| public static class Container { |
| |
| public Container a; |
| public Container b; |
| } |
| |
| private static native void safepoint(); |
| |
| public static void test1Snippet(Container main) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| barrierIndex = 0; |
| safepoint(); |
| barrierIndex = 1; |
| main.a = temp1; |
| safepoint(); |
| barrierIndex = 2; |
| main.b = temp2; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test1() { |
| test("test1Snippet", 2, new int[]{1}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test2() { |
| test("test1Snippet", 2, new int[]{2}); |
| } |
| |
| public static void test2Snippet(Container main) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| barrierIndex = 0; |
| safepoint(); |
| barrierIndex = 1; |
| main.a = temp1; |
| barrierIndex = 2; |
| main.b = temp2; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test3() { |
| test("test2Snippet", 2, new int[]{1}); |
| } |
| |
| @Test |
| public void test4() { |
| test("test2Snippet", 2, new int[]{2}); |
| } |
| |
| public static void test3Snippet(Container main, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| barrierIndex = 0; |
| safepoint(); |
| for (int i = 0; i < 10; i++) { |
| if (test) { |
| barrierIndex = 1; |
| main.a = temp1; |
| barrierIndex = 2; |
| main.b = temp2; |
| } else { |
| barrierIndex = 3; |
| main.a = temp1; |
| barrierIndex = 4; |
| main.b = temp2; |
| } |
| } |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test5() { |
| test("test3Snippet", 4, new int[]{1, 2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test6() { |
| test("test3Snippet", 4, new int[]{3, 4}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test7() { |
| test("test3Snippet", 4, new int[]{1}); |
| } |
| |
| @Test |
| public void test8() { |
| test("test3Snippet", 4, new int[]{2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test9() { |
| test("test3Snippet", 4, new int[]{3}); |
| } |
| |
| @Test |
| public void test10() { |
| test("test3Snippet", 4, new int[]{4}); |
| } |
| |
| public static void test4Snippet(Container main, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| barrierIndex = 1; |
| main.a = temp1; |
| for (int i = 0; i < 10; i++) { |
| if (test) { |
| barrierIndex = 2; |
| main.a = temp1; |
| barrierIndex = 3; |
| main.b = temp2; |
| } else { |
| barrierIndex = 4; |
| main.a = temp2; |
| barrierIndex = 5; |
| main.b = temp1; |
| } |
| } |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test11() { |
| test("test4Snippet", 5, new int[]{2, 3}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test12() { |
| test("test4Snippet", 5, new int[]{4, 5}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test13() { |
| test("test4Snippet", 5, new int[]{1}); |
| } |
| |
| public static void test5Snippet(Container main) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| barrierIndex = 1; |
| main.a = temp1; |
| if (main.a == main.b) { |
| barrierIndex = 2; |
| main.a = temp1; |
| barrierIndex = 3; |
| main.b = temp2; |
| } else { |
| barrierIndex = 4; |
| main.a = temp2; |
| barrierIndex = 5; |
| main.b = temp1; |
| } |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test14() { |
| test("test5Snippet", 5, new int[]{1}); |
| } |
| |
| @Test |
| public void test15() { |
| test("test5Snippet", 5, new int[]{2}); |
| } |
| |
| @Test |
| public void test16() { |
| test("test5Snippet", 5, new int[]{4}); |
| } |
| |
| @Test |
| public void test17() { |
| test("test5Snippet", 5, new int[]{3}); |
| } |
| |
| @Test |
| public void test18() { |
| test("test5Snippet", 5, new int[]{5}); |
| } |
| |
| @Test |
| public void test19() { |
| test("test5Snippet", 5, new int[]{2, 3}); |
| } |
| |
| @Test |
| public void test20() { |
| test("test5Snippet", 5, new int[]{4, 5}); |
| } |
| |
| public static void test6Snippet(Container main, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| barrierIndex = 1; |
| main.a = temp1; |
| if (test) { |
| barrierIndex = 2; |
| main.a = temp1; |
| barrierIndex = 3; |
| main.b = temp1.a.a; |
| } else { |
| barrierIndex = 4; |
| main.a = temp2; |
| barrierIndex = 5; |
| main.b = temp2.a.a; |
| } |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test21() { |
| test("test6Snippet", 5, new int[]{1}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test22() { |
| test("test6Snippet", 5, new int[]{1, 2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test23() { |
| test("test6Snippet", 5, new int[]{3}); |
| } |
| |
| @Test |
| public void test24() { |
| test("test6Snippet", 5, new int[]{4}); |
| } |
| |
| public static void test7Snippet(Container main, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| barrierIndex = 1; |
| main.a = temp1; |
| if (test) { |
| barrierIndex = 2; |
| main.a = temp1; |
| } |
| barrierIndex = 3; |
| main.b = temp2; |
| safepoint(); |
| } |
| |
| @Test |
| public void test25() { |
| test("test7Snippet", 3, new int[]{2}); |
| } |
| |
| @Test |
| public void test26() { |
| test("test7Snippet", 3, new int[]{3}); |
| } |
| |
| @Test |
| public void test27() { |
| test("test7Snippet", 3, new int[]{2, 3}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test28() { |
| test("test7Snippet", 3, new int[]{1}); |
| } |
| |
| public static void test8Snippet(Container main, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| if (test) { |
| barrierIndex = 1; |
| main.a = temp1; |
| } |
| barrierIndex = 2; |
| main.b = temp2; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test29() { |
| test("test8Snippet", 2, new int[]{1}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test30() { |
| test("test8Snippet", 2, new int[]{2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test31() { |
| test("test8Snippet", 2, new int[]{1, 2}); |
| } |
| |
| public static void test9Snippet(Container main1, Container main2, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| if (test) { |
| barrierIndex = 1; |
| main1.a = temp1; |
| } else { |
| barrierIndex = 2; |
| main2.a = temp1; |
| } |
| barrierIndex = 3; |
| main1.b = temp2; |
| barrierIndex = 4; |
| main2.b = temp2; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test32() { |
| test("test9Snippet", 4, new int[]{1}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test33() { |
| test("test9Snippet", 4, new int[]{2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test34() { |
| test("test9Snippet", 4, new int[]{3}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test35() { |
| test("test9Snippet", 4, new int[]{4}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test36() { |
| test("test9Snippet", 4, new int[]{1, 2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test37() { |
| test("test9Snippet", 4, new int[]{3, 4}); |
| } |
| |
| public static void test10Snippet(Container main1, Container main2, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| if (test) { |
| barrierIndex = 1; |
| main1.a = temp1; |
| barrierIndex = 2; |
| main2.a = temp2; |
| } else { |
| barrierIndex = 3; |
| main2.a = temp1; |
| } |
| barrierIndex = 4; |
| main1.b = temp2; |
| barrierIndex = 5; |
| main2.b = temp2; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test38() { |
| test("test10Snippet", 5, new int[]{1}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test39() { |
| test("test10Snippet", 5, new int[]{2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test40() { |
| test("test10Snippet", 5, new int[]{3}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test41() { |
| test("test10Snippet", 5, new int[]{4}); |
| } |
| |
| @Test |
| public void test42() { |
| test("test10Snippet", 5, new int[]{5}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test43() { |
| test("test10Snippet", 5, new int[]{1, 2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test44() { |
| test("test10Snippet", 5, new int[]{1, 2, 3}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test45() { |
| test("test10Snippet", 5, new int[]{3, 4}); |
| } |
| |
| public static void test11Snippet(Container main1, Container main2, Container main3, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| safepoint(); |
| if (test) { |
| barrierIndex = 1; |
| main1.a = temp1; |
| barrierIndex = 2; |
| main3.a = temp1; |
| if (!test) { |
| barrierIndex = 3; |
| main2.a = temp2; |
| } else { |
| barrierIndex = 4; |
| main1.a = temp2; |
| barrierIndex = 5; |
| main3.a = temp2; |
| } |
| } else { |
| barrierIndex = 6; |
| main1.b = temp2; |
| for (int i = 0; i < 10; i++) { |
| barrierIndex = 7; |
| main3.a = temp1; |
| } |
| barrierIndex = 8; |
| main3.b = temp2; |
| } |
| barrierIndex = 9; |
| main1.b = temp2; |
| barrierIndex = 10; |
| main2.b = temp2; |
| barrierIndex = 11; |
| main3.b = temp2; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test46() { |
| test("test11Snippet", 11, new int[]{1}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test47() { |
| test("test11Snippet", 11, new int[]{2}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test48() { |
| test("test11Snippet", 11, new int[]{3}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test49() { |
| test("test11Snippet", 11, new int[]{6}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test50() { |
| test("test11Snippet", 11, new int[]{7}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test51() { |
| test("test11Snippet", 11, new int[]{8}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test52() { |
| test("test11Snippet", 11, new int[]{9}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test53() { |
| test("test11Snippet", 11, new int[]{10}); |
| } |
| |
| @Test |
| public void test54() { |
| test("test11Snippet", 11, new int[]{4}); |
| } |
| |
| @Test |
| public void test55() { |
| test("test11Snippet", 11, new int[]{5}); |
| } |
| |
| @Test |
| public void test56() { |
| test("test11Snippet", 11, new int[]{11}); |
| } |
| |
| public static void test12Snippet(Container main, Container main1, boolean test) { |
| Container temp1 = new Container(); |
| Container temp2 = new Container(); |
| barrierIndex = 0; |
| safepoint(); |
| barrierIndex = 7; |
| main1.a = temp1; |
| for (int i = 0; i < 10; i++) { |
| if (test) { |
| barrierIndex = 1; |
| main.a = temp1; |
| barrierIndex = 2; |
| main.b = temp2; |
| } else { |
| barrierIndex = 3; |
| main.a = temp1; |
| barrierIndex = 4; |
| main.b = temp2; |
| } |
| } |
| barrierIndex = 5; |
| main.a = temp1; |
| barrierIndex = 6; |
| main.b = temp1; |
| barrierIndex = 8; |
| main1.b = temp1; |
| safepoint(); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test57() { |
| test("test12Snippet", 8, new int[]{5}); |
| } |
| |
| @Test |
| public void test58() { |
| test("test12Snippet", 8, new int[]{6}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test59() { |
| test("test12Snippet", 8, new int[]{7}); |
| } |
| |
| @Test(expected = AssertionError.class) |
| public void test60() { |
| test("test12Snippet", 8, new int[]{8}); |
| } |
| |
| public static void test13Snippet(Object[] a, Object[] b) { |
| System.arraycopy(a, 0, b, 0, a.length); |
| } |
| |
| @Test |
| public void test61() { |
| GraphPredicate checkForUnsafeArrayCopy = graph -> graph.getNodes().filter(UnsafeArrayCopyNode.class).count() > 0 ? 1 : 0; |
| testPredicate("test13Snippet", checkForUnsafeArrayCopy, new int[]{}); |
| } |
| |
| private interface GraphPredicate { |
| int apply(StructuredGraph graph); |
| } |
| |
| private void test(final String snippet, final int expectedBarriers, final int... removedBarrierIndices) { |
| GraphPredicate noCheck = noArg -> expectedBarriers; |
| testPredicate(snippet, noCheck, removedBarrierIndices); |
| } |
| |
| @SuppressWarnings("try") |
| private void testPredicate(final String snippet, final GraphPredicate expectedBarriers, final int... removedBarrierIndices) { |
| try (Scope d = Debug.scope("WriteBarrierVerificationTest", new DebugDumpScope(snippet))) { |
| final StructuredGraph graph = parseEager(snippet, AllowAssumptions.YES); |
| HighTierContext highTierContext = getDefaultHighTierContext(); |
| new InliningPhase(new CanonicalizerPhase()).apply(graph, highTierContext); |
| |
| MidTierContext midTierContext = new MidTierContext(getProviders(), getTargetProvider(), OptimisticOptimizations.ALL, graph.getProfilingInfo()); |
| |
| new LoweringPhase(new CanonicalizerPhase(), LoweringTool.StandardLoweringStage.HIGH_TIER).apply(graph, highTierContext); |
| new GuardLoweringPhase().apply(graph, midTierContext); |
| new LoopSafepointInsertionPhase().apply(graph); |
| new LoweringPhase(new CanonicalizerPhase(), LoweringTool.StandardLoweringStage.MID_TIER).apply(graph, highTierContext); |
| |
| new WriteBarrierAdditionPhase(config).apply(graph); |
| |
| int barriers = 0; |
| // First, the total number of expected barriers is checked. |
| if (config.useG1GC) { |
| barriers = graph.getNodes().filter(G1PreWriteBarrier.class).count() + graph.getNodes().filter(G1PostWriteBarrier.class).count() + |
| graph.getNodes().filter(G1ArrayRangePreWriteBarrier.class).count() + graph.getNodes().filter(G1ArrayRangePostWriteBarrier.class).count(); |
| Assert.assertTrue(expectedBarriers.apply(graph) * 2 == barriers); |
| } else { |
| barriers = graph.getNodes().filter(SerialWriteBarrier.class).count() + graph.getNodes().filter(SerialArrayRangeWriteBarrier.class).count(); |
| Assert.assertTrue(expectedBarriers.apply(graph) == barriers); |
| } |
| ResolvedJavaField barrierIndexField = getMetaAccess().lookupJavaField(WriteBarrierVerificationTest.class.getDeclaredField("barrierIndex")); |
| LocationIdentity barrierIdentity = new FieldLocationIdentity(barrierIndexField); |
| // Iterate over all write nodes and remove barriers according to input indices. |
| NodeIteratorClosure<Boolean> closure = new NodeIteratorClosure<Boolean>() { |
| |
| @Override |
| protected Boolean processNode(FixedNode node, Boolean currentState) { |
| if (node instanceof WriteNode) { |
| WriteNode write = (WriteNode) node; |
| LocationIdentity obj = write.getLocationIdentity(); |
| if (obj.equals(barrierIdentity)) { |
| /* |
| * A "barrierIndex" variable was found and is checked against the input |
| * barrier array. |
| */ |
| if (eliminateBarrier(write.value().asJavaConstant().asInt(), removedBarrierIndices)) { |
| return true; |
| } |
| } |
| } else if (node instanceof SerialWriteBarrier || node instanceof G1PostWriteBarrier) { |
| // Remove flagged write barriers. |
| if (currentState) { |
| graph.removeFixed(((FixedWithNextNode) node)); |
| return false; |
| } |
| } |
| return currentState; |
| } |
| |
| private boolean eliminateBarrier(int index, int[] map) { |
| for (int i = 0; i < map.length; i++) { |
| if (map[i] == index) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| protected Map<LoopExitNode, Boolean> processLoop(LoopBeginNode loop, Boolean initialState) { |
| return ReentrantNodeIterator.processLoop(this, loop, initialState).exitStates; |
| } |
| |
| @Override |
| protected Boolean merge(AbstractMergeNode merge, List<Boolean> states) { |
| return false; |
| } |
| |
| @Override |
| protected Boolean afterSplit(AbstractBeginNode node, Boolean oldState) { |
| return false; |
| } |
| }; |
| |
| DebugConfig debugConfig = DebugScope.getConfig(); |
| DebugConfig fixedConfig = debugConfig == null ? null |
| : Debug.fixedConfig(0, 0, false, false, false, false, false, debugConfig.dumpHandlers(), debugConfig.verifyHandlers(), debugConfig.output()); |
| try (DebugConfigScope s = Debug.setConfig(fixedConfig)) { |
| ReentrantNodeIterator.apply(closure, graph.start(), false); |
| new WriteBarrierVerificationPhase(config).apply(graph); |
| } catch (AssertionError error) { |
| /* |
| * Catch assertion, test for expected one and re-throw in order to validate unit |
| * test. |
| */ |
| Assert.assertTrue(error.getMessage().contains("Write barrier must be present")); |
| throw error; |
| } |
| } catch (Throwable e) { |
| throw Debug.handle(e); |
| } |
| } |
| } |