1 /* 2 * Copyright (c) 2013, 2018, 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 import com.sun.management.HotSpotDiagnosticMXBean; 25 import com.sun.management.VMOption; 26 import sun.hotspot.WhiteBox; 27 import sun.hotspot.code.NMethod; 28 import sun.management.ManagementFactoryHelper; 29 30 import java.lang.reflect.Constructor; 31 import java.lang.reflect.Executable; 32 import java.lang.reflect.Method; 33 import java.util.Objects; 34 import java.util.concurrent.Callable; 35 import java.util.function.Function; 36 37 /** 38 * Abstract class for WhiteBox testing of JIT. 39 * 40 * @author igor.ignatyev@oracle.com 41 */ 42 public abstract class CompilerWhiteBoxTest { 43 /** {@code CompLevel::CompLevel_none} -- Interpreter */ 44 protected static int COMP_LEVEL_NONE = 0; 45 /** {@code CompLevel::CompLevel_any}, {@code CompLevel::CompLevel_all} */ 46 protected static int COMP_LEVEL_ANY = -1; 47 /** {@code CompLevel::CompLevel_simple} -- C1 */ 48 protected static int COMP_LEVEL_SIMPLE = 1; 49 /** {@code CompLevel::CompLevel_limited_profile} -- C1, invocation & backedge counters */ 50 protected static int COMP_LEVEL_LIMITED_PROFILE = 2; 51 /** {@code CompLevel::CompLevel_full_profile} -- C1, invocation & backedge counters + mdo */ 52 protected static int COMP_LEVEL_FULL_PROFILE = 3; 53 /** {@code CompLevel::CompLevel_full_optimization} -- C2 or Shark */ 54 protected static int COMP_LEVEL_FULL_OPTIMIZATION = 4; 55 /** Maximal value for CompLevel */ 56 protected static int COMP_LEVEL_MAX = COMP_LEVEL_FULL_OPTIMIZATION; 57 58 /** Instance of WhiteBox */ 59 protected static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); 60 /** Value of {@code -XX:CompileThreshold} */ 61 protected static final int COMPILE_THRESHOLD 62 = Integer.parseInt(getVMOption("CompileThreshold", "10000")); 63 /** Value of {@code -XX:BackgroundCompilation} */ 64 protected static final boolean BACKGROUND_COMPILATION 65 = Boolean.valueOf(getVMOption("BackgroundCompilation", "true")); 66 /** Value of {@code -XX:TieredCompilation} */ 67 protected static final boolean TIERED_COMPILATION 68 = Boolean.valueOf(getVMOption("TieredCompilation", "false")); 69 /** Value of {@code -XX:TieredStopAtLevel} */ 70 protected static final int TIERED_STOP_AT_LEVEL 71 = Integer.parseInt(getVMOption("TieredStopAtLevel", "0")); 72 /** Flag for verbose output, true if {@code -Dverbose} specified */ 73 protected static final boolean IS_VERBOSE 74 = System.getProperty("verbose") != null; 75 /** invocation count to trigger compilation */ 76 protected static final int THRESHOLD; 77 /** invocation count to trigger OSR compilation */ 78 protected static final long BACKEDGE_THRESHOLD; 79 /** Value of {@code java.vm.info} (interpreted|mixed|comp mode) */ 80 protected static final String MODE = System.getProperty("java.vm.info"); 81 82 static { 83 if (TIERED_COMPILATION) { 84 BACKEDGE_THRESHOLD = THRESHOLD = 150000; 85 } else { 86 THRESHOLD = COMPILE_THRESHOLD; 87 BACKEDGE_THRESHOLD = Math.max(10000, COMPILE_THRESHOLD * 88 Long.parseLong(getVMOption("OnStackReplacePercentage"))); 89 } 90 } 91 92 /** 93 * Returns value of VM option. 94 * 95 * @param name option's name 96 * @return value of option or {@code null}, if option doesn't exist 97 * @throws NullPointerException if name is null 98 */ 99 protected static String getVMOption(String name) { 100 Objects.requireNonNull(name); 101 HotSpotDiagnosticMXBean diagnostic 102 = ManagementFactoryHelper.getDiagnosticMXBean(); 103 VMOption tmp; 104 try { 105 tmp = diagnostic.getVMOption(name); 106 } catch (IllegalArgumentException e) { 107 tmp = null; 108 } 109 return (tmp == null ? null : tmp.getValue()); 110 } 111 112 /** 113 * Returns value of VM option or default value. 114 * 115 * @param name option's name 116 * @param defaultValue default value 117 * @return value of option or {@code defaultValue}, if option doesn't exist 118 * @throws NullPointerException if name is null 119 * @see #getVMOption(String) 120 */ 121 protected static String getVMOption(String name, String defaultValue) { 122 String result = getVMOption(name); 123 return result == null ? defaultValue : result; 124 } 125 126 /** copy of is_c1_compile(int) from utilities/globalDefinitions.hpp */ 127 protected static boolean isC1Compile(int compLevel) { 128 return (compLevel > COMP_LEVEL_NONE) 129 && (compLevel < COMP_LEVEL_FULL_OPTIMIZATION); 130 } 131 132 /** copy of is_c2_compile(int) from utilities/globalDefinitions.hpp */ 133 protected static boolean isC2Compile(int compLevel) { 134 return compLevel == COMP_LEVEL_FULL_OPTIMIZATION; 135 } 136 137 protected static void main( 138 Function<TestCase, CompilerWhiteBoxTest> constructor, 139 String[] args) { 140 if (args.length == 0) { 141 for (TestCase test : SimpleTestCase.values()) { 142 constructor.apply(test).runTest(); 143 } 144 } else { 145 for (String name : args) { 146 constructor.apply(SimpleTestCase.valueOf(name)).runTest(); 147 } 148 } 149 } 150 151 /** tested method */ 152 protected final Executable method; 153 protected final TestCase testCase; 154 155 /** 156 * Constructor. 157 * 158 * @param testCase object, that contains tested method and way to invoke it. 159 */ 160 protected CompilerWhiteBoxTest(TestCase testCase) { 161 Objects.requireNonNull(testCase); 162 System.out.println("TEST CASE:" + testCase.name()); 163 method = testCase.getExecutable(); 164 this.testCase = testCase; 165 } 166 167 /** 168 * Template method for testing. Prints tested method's info before 169 * {@linkplain #test()} and after {@linkplain #test()} or on thrown 170 * exception. 171 * 172 * @throws RuntimeException if method {@linkplain #test()} throws any 173 * exception 174 * @see #test() 175 */ 176 protected final void runTest() { 177 if (ManagementFactoryHelper.getCompilationMXBean() == null) { 178 System.err.println( 179 "Warning: test is not applicable in interpreted mode"); 180 return; 181 } 182 System.out.println("at test's start:"); 183 printInfo(); 184 try { 185 test(); 186 } catch (Exception e) { 187 System.out.printf("on exception '%s':", e.getMessage()); 188 printInfo(); 189 e.printStackTrace(); 190 if (e instanceof RuntimeException) { 191 throw (RuntimeException) e; 192 } 193 throw new RuntimeException(e); 194 } 195 System.out.println("at test's end:"); 196 printInfo(); 197 } 198 199 /** 200 * Checks, that {@linkplain #method} is not compiled at the given compilation 201 * level or above. 202 * 203 * @param compLevel 204 * 205 * @throws RuntimeException if {@linkplain #method} is in compiler queue or 206 * is compiled, or if {@linkplain #method} has zero 207 * compilation level. 208 */ 209 protected final void checkNotCompiled(int compLevel) { 210 if (WHITE_BOX.isMethodQueuedForCompilation(method)) { 211 throw new RuntimeException(method + " must not be in queue"); 212 } 213 if (WHITE_BOX.getMethodCompilationLevel(method, false) >= compLevel) { 214 throw new RuntimeException(method + " comp_level must be >= maxCompLevel"); 215 } 216 if (WHITE_BOX.getMethodCompilationLevel(method, true) >= compLevel) { 217 throw new RuntimeException(method + " osr_comp_level must be >= maxCompLevel"); 218 } 219 } 220 221 /** 222 * Checks, that {@linkplain #method} is not compiled. 223 * 224 * @throws RuntimeException if {@linkplain #method} is in compiler queue or 225 * is compiled, or if {@linkplain #method} has zero 226 * compilation level. 227 */ 228 protected final void checkNotCompiled() { 229 checkNotCompiled(true); 230 checkNotCompiled(false); 231 } 232 233 /** 234 * Checks, that {@linkplain #method} is not (OSR-)compiled. 235 * 236 * @param isOsr Check for OSR compilation if true 237 * @throws RuntimeException if {@linkplain #method} is in compiler queue or 238 * is compiled, or if {@linkplain #method} has zero 239 * compilation level. 240 */ 241 protected final void checkNotCompiled(boolean isOsr) { 242 waitBackgroundCompilation(); 243 if (WHITE_BOX.isMethodQueuedForCompilation(method)) { 244 throw new RuntimeException(method + " must not be in queue"); 245 } 246 if (WHITE_BOX.isMethodCompiled(method, isOsr)) { 247 throw new RuntimeException(method + " must not be " + 248 (isOsr ? "osr_" : "") + "compiled"); 249 } 250 if (WHITE_BOX.getMethodCompilationLevel(method, isOsr) != 0) { 251 throw new RuntimeException(method + (isOsr ? " osr_" : " ") + 252 "comp_level must be == 0"); 253 } 254 } 255 256 /** 257 * Checks, that {@linkplain #method} is compiled. 258 * 259 * @throws RuntimeException if {@linkplain #method} isn't in compiler queue 260 * and isn't compiled, or if {@linkplain #method} 261 * has nonzero compilation level 262 */ 263 protected final void checkCompiled() { 264 final long start = System.currentTimeMillis(); 265 waitBackgroundCompilation(); 266 if (WHITE_BOX.isMethodQueuedForCompilation(method)) { 267 System.err.printf("Warning: %s is still in queue after %dms%n", 268 method, System.currentTimeMillis() - start); 269 return; 270 } 271 if (!WHITE_BOX.isMethodCompiled(method, testCase.isOsr())) { 272 throw new RuntimeException(method + " must be " 273 + (testCase.isOsr() ? "osr_" : "") + "compiled"); 274 } 275 if (WHITE_BOX.getMethodCompilationLevel(method, testCase.isOsr()) 276 == 0) { 277 throw new RuntimeException(method 278 + (testCase.isOsr() ? " osr_" : " ") 279 + "comp_level must be != 0"); 280 } 281 } 282 283 protected final void deoptimize() { 284 WHITE_BOX.deoptimizeMethod(method, testCase.isOsr()); 285 if (testCase.isOsr()) { 286 WHITE_BOX.deoptimizeMethod(method, false); 287 } 288 } 289 290 protected final int getCompLevel() { 291 NMethod nm = NMethod.get(method, testCase.isOsr()); 292 return nm == null ? COMP_LEVEL_NONE : nm.comp_level; 293 } 294 295 protected final boolean isCompilable() { 296 return WHITE_BOX.isMethodCompilable(method, COMP_LEVEL_ANY, 297 testCase.isOsr()); 298 } 299 300 protected final boolean isCompilable(int compLevel) { 301 return WHITE_BOX 302 .isMethodCompilable(method, compLevel, testCase.isOsr()); 303 } 304 305 protected final void makeNotCompilable() { 306 WHITE_BOX.makeMethodNotCompilable(method, COMP_LEVEL_ANY, 307 testCase.isOsr()); 308 } 309 310 protected final void makeNotCompilable(int compLevel) { 311 WHITE_BOX.makeMethodNotCompilable(method, compLevel, testCase.isOsr()); 312 } 313 314 /** 315 * Waits for completion of background compilation of {@linkplain #method}. 316 */ 317 protected final void waitBackgroundCompilation() { 318 waitBackgroundCompilation(method); 319 } 320 321 /** 322 * Waits for completion of background compilation of the given executable. 323 * 324 * @param executable Executable 325 */ 326 protected static final void waitBackgroundCompilation(Executable executable) { 327 if (!BACKGROUND_COMPILATION) { 328 return; 329 } 330 final Object obj = new Object(); 331 for (int i = 0; i < 10 332 && WHITE_BOX.isMethodQueuedForCompilation(executable); ++i) { 333 synchronized (obj) { 334 try { 335 obj.wait(1000); 336 } catch (InterruptedException e) { 337 Thread.currentThread().interrupt(); 338 } 339 } 340 } 341 } 342 343 /** 344 * Prints information about {@linkplain #method}. 345 */ 346 protected final void printInfo() { 347 System.out.printf("%n%s:%n", method); 348 System.out.printf("\tcompilable:\t%b%n", 349 WHITE_BOX.isMethodCompilable(method, COMP_LEVEL_ANY, false)); 350 System.out.printf("\tcompiled:\t%b%n", 351 WHITE_BOX.isMethodCompiled(method, false)); 352 System.out.printf("\tcomp_level:\t%d%n", 353 WHITE_BOX.getMethodCompilationLevel(method, false)); 354 System.out.printf("\tosr_compilable:\t%b%n", 355 WHITE_BOX.isMethodCompilable(method, COMP_LEVEL_ANY, true)); 356 System.out.printf("\tosr_compiled:\t%b%n", 357 WHITE_BOX.isMethodCompiled(method, true)); 358 System.out.printf("\tosr_comp_level:\t%d%n", 359 WHITE_BOX.getMethodCompilationLevel(method, true)); 360 System.out.printf("\tin_queue:\t%b%n", 361 WHITE_BOX.isMethodQueuedForCompilation(method)); 362 System.out.printf("compile_queues_size:\t%d%n%n", 363 WHITE_BOX.getCompileQueuesSize()); 364 } 365 366 /** 367 * Executes testing. 368 */ 369 protected abstract void test() throws Exception; 370 371 /** 372 * Tries to trigger compilation of {@linkplain #method} by call 373 * {@linkplain TestCase#getCallable()} enough times. 374 * 375 * @return accumulated result 376 * @see #compile(int) 377 */ 378 protected final int compile() { 379 if (testCase.isOsr()) { 380 return compile(1); 381 } else { 382 return compile(THRESHOLD); 383 } 384 } 385 386 /** 387 * Tries to trigger compilation of {@linkplain #method} by call 388 * {@linkplain TestCase#getCallable()} specified times. 389 * 390 * @param count invocation count 391 * @return accumulated result 392 */ 393 protected final int compile(int count) { 394 int result = 0; 395 Integer tmp; 396 for (int i = 0; i < count; ++i) { 397 try { 398 tmp = testCase.getCallable().call(); 399 } catch (Exception e) { 400 tmp = null; 401 } 402 result += tmp == null ? 0 : tmp; 403 } 404 if (IS_VERBOSE) { 405 System.out.println("method was invoked " + count + " times"); 406 } 407 return result; 408 } 409 410 /** 411 * Utility interface provides tested method and object to invoke it. 412 */ 413 public interface TestCase { 414 /** the name of test case */ 415 String name(); 416 417 /** tested method */ 418 Executable getExecutable(); 419 420 /** object to invoke {@linkplain #getExecutable()} */ 421 Callable<Integer> getCallable(); 422 423 /** flag for OSR test case */ 424 boolean isOsr(); 425 } 426 427 /** 428 * @return {@code true} if the current test case is OSR and the mode is 429 * Xcomp, otherwise {@code false} 430 */ 431 protected boolean skipXcompOSR() { 432 boolean result = testCase.isOsr() 433 && CompilerWhiteBoxTest.MODE.startsWith("compiled "); 434 if (result && IS_VERBOSE) { 435 System.err.printf("Warning: %s is not applicable in %s%n", 436 testCase.name(), CompilerWhiteBoxTest.MODE); 437 } 438 return result; 439 } 440 } 441 442 enum SimpleTestCase implements CompilerWhiteBoxTest.TestCase { 443 /** constructor test case */ 444 CONSTRUCTOR_TEST(Helper.CONSTRUCTOR, Helper.CONSTRUCTOR_CALLABLE, false), 445 /** method test case */ 446 METHOD_TEST(Helper.METHOD, Helper.METHOD_CALLABLE, false), 447 /** static method test case */ 448 STATIC_TEST(Helper.STATIC, Helper.STATIC_CALLABLE, false), 449 /** OSR constructor test case */ 450 OSR_CONSTRUCTOR_TEST(Helper.OSR_CONSTRUCTOR, 451 Helper.OSR_CONSTRUCTOR_CALLABLE, true), 452 /** OSR method test case */ 453 OSR_METHOD_TEST(Helper.OSR_METHOD, Helper.OSR_METHOD_CALLABLE, true), 454 /** OSR static method test case */ 455 OSR_STATIC_TEST(Helper.OSR_STATIC, Helper.OSR_STATIC_CALLABLE, true); 456 457 private final Executable executable; 458 private final Callable<Integer> callable; 459 private final boolean isOsr; 460 461 private SimpleTestCase(Executable executable, Callable<Integer> callable, 462 boolean isOsr) { 463 this.executable = executable; 464 this.callable = callable; 465 this.isOsr = isOsr; 466 } 467 468 @Override 469 public Executable getExecutable() { 470 return executable; 471 } 472 473 @Override 474 public Callable<Integer> getCallable() { 475 return callable; 476 } 477 478 @Override 479 public boolean isOsr() { 480 return isOsr; 481 } 482 483 private static class Helper { 484 485 private static final Callable<Integer> CONSTRUCTOR_CALLABLE 486 = new Callable<Integer>() { 487 @Override 488 public Integer call() throws Exception { 489 return new Helper(1337).hashCode(); 490 } 491 }; 492 493 private static final Callable<Integer> METHOD_CALLABLE 494 = new Callable<Integer>() { 495 private final Helper helper = new Helper(); 496 497 @Override 498 public Integer call() throws Exception { 499 return helper.method(); 500 } 501 }; 502 503 private static final Callable<Integer> STATIC_CALLABLE 504 = new Callable<Integer>() { 505 @Override 506 public Integer call() throws Exception { 507 return staticMethod(); 508 } 509 }; 510 511 private static final Callable<Integer> OSR_CONSTRUCTOR_CALLABLE 512 = new Callable<Integer>() { 513 @Override 514 public Integer call() throws Exception { 515 return new Helper(null, CompilerWhiteBoxTest.BACKEDGE_THRESHOLD).hashCode(); 516 } 517 }; 518 519 private static final Callable<Integer> OSR_METHOD_CALLABLE 520 = new Callable<Integer>() { 521 private final Helper helper = new Helper(); 522 523 @Override 524 public Integer call() throws Exception { 525 return helper.osrMethod(CompilerWhiteBoxTest.BACKEDGE_THRESHOLD); 526 } 527 }; 528 529 private static final Callable<Integer> OSR_STATIC_CALLABLE 530 = new Callable<Integer>() { 531 @Override 532 public Integer call() throws Exception { 533 return osrStaticMethod(CompilerWhiteBoxTest.BACKEDGE_THRESHOLD); 534 } 535 }; 536 537 private static final Constructor CONSTRUCTOR; 538 private static final Constructor OSR_CONSTRUCTOR; 539 private static final Method METHOD; 540 private static final Method STATIC; 541 private static final Method OSR_METHOD; 542 private static final Method OSR_STATIC; 543 544 static { 545 try { 546 CONSTRUCTOR = Helper.class.getDeclaredConstructor(int.class); 547 } catch (NoSuchMethodException | SecurityException e) { 548 throw new RuntimeException( 549 "exception on getting method Helper.<init>(int)", e); 550 } 551 try { 552 OSR_CONSTRUCTOR = Helper.class.getDeclaredConstructor( 553 Object.class, long.class); 554 } catch (NoSuchMethodException | SecurityException e) { 555 throw new RuntimeException( 556 "exception on getting method Helper.<init>(Object, long)", e); 557 } 558 METHOD = getMethod("method"); 559 STATIC = getMethod("staticMethod"); 560 OSR_METHOD = getMethod("osrMethod", long.class); 561 OSR_STATIC = getMethod("osrStaticMethod", long.class); 562 } 563 564 private static Method getMethod(String name, Class<?>... parameterTypes) { 565 try { 566 return Helper.class.getDeclaredMethod(name, parameterTypes); 567 } catch (NoSuchMethodException | SecurityException e) { 568 throw new RuntimeException( 569 "exception on getting method Helper." + name, e); 570 } 571 } 572 573 private static int staticMethod() { 574 return 1138; 575 } 576 577 private int method() { 578 return 42; 579 } 580 581 /** 582 * Deoptimizes all non-osr versions of the given executable after 583 * compilation finished. 584 * 585 * @param e Executable 586 * @throws Exception 587 */ 588 private static void waitAndDeoptimize(Executable e) { 589 CompilerWhiteBoxTest.waitBackgroundCompilation(e); 590 if (WhiteBox.getWhiteBox().isMethodQueuedForCompilation(e)) { 591 throw new RuntimeException(e + " must not be in queue"); 592 } 593 // Deoptimize non-osr versions of executable 594 WhiteBox.getWhiteBox().deoptimizeMethod(e, false); 595 } 596 597 /** 598 * Executes the method multiple times to make sure we have 599 * enough profiling information before triggering an OSR 600 * compilation. Otherwise the C2 compiler may add uncommon traps. 601 * 602 * @param m Method to be executed 603 * @return Number of times the method was executed 604 * @throws Exception 605 */ 606 private static int warmup(Method m) throws Exception { 607 waitAndDeoptimize(m); 608 Helper helper = new Helper(); 609 int result = 0; 610 for (long i = 0; i < CompilerWhiteBoxTest.THRESHOLD; ++i) { 611 result += (int)m.invoke(helper, 1); 612 } 613 // Wait to make sure OSR compilation is not blocked by 614 // non-OSR compilation in the compile queue 615 CompilerWhiteBoxTest.waitBackgroundCompilation(m); 616 return result; 617 } 618 619 /** 620 * Executes the constructor multiple times to make sure we 621 * have enough profiling information before triggering an OSR 622 * compilation. Otherwise the C2 compiler may add uncommon traps. 623 * 624 * @param c Constructor to be executed 625 * @return Number of times the constructor was executed 626 * @throws Exception 627 */ 628 private static int warmup(Constructor c) throws Exception { 629 waitAndDeoptimize(c); 630 int result = 0; 631 for (long i = 0; i < CompilerWhiteBoxTest.THRESHOLD; ++i) { 632 result += c.newInstance(null, 1).hashCode(); 633 } 634 // Wait to make sure OSR compilation is not blocked by 635 // non-OSR compilation in the compile queue 636 CompilerWhiteBoxTest.waitBackgroundCompilation(c); 637 return result; 638 } 639 640 private static int osrStaticMethod(long limit) throws Exception { 641 int result = 0; 642 if (limit != 1) { 643 result = warmup(OSR_STATIC); 644 } 645 // Trigger osr compilation 646 for (long i = 0; i < limit; ++i) { 647 result += staticMethod(); 648 } 649 return result; 650 } 651 652 private int osrMethod(long limit) throws Exception { 653 int result = 0; 654 if (limit != 1) { 655 result = warmup(OSR_METHOD); 656 } 657 // Trigger osr compilation 658 for (long i = 0; i < limit; ++i) { 659 result += method(); 660 } 661 return result; 662 } 663 664 private final int x; 665 666 // for method and OSR method test case 667 public Helper() { 668 x = 0; 669 } 670 671 // for OSR constructor test case 672 private Helper(Object o, long limit) throws Exception { 673 int result = 0; 674 if (limit != 1) { 675 result = warmup(OSR_CONSTRUCTOR); 676 } 677 // Trigger osr compilation 678 for (long i = 0; i < limit; ++i) { 679 result += method(); 680 } 681 x = result; 682 } 683 684 // for constructor test case 685 private Helper(int x) { 686 this.x = x; 687 } 688 689 @Override 690 public int hashCode() { 691 return x; 692 } 693 } 694 }