| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 #include "platform/address_sanitizer.h" | 5 #include "platform/address_sanitizer.h" |
| 6 #include "platform/memory_sanitizer.h" | 6 #include "platform/memory_sanitizer.h" |
| 7 #include "platform/utils.h" | 7 #include "platform/utils.h" |
| 8 | 8 |
| 9 #include "vm/allocation.h" | 9 #include "vm/allocation.h" |
| 10 #include "vm/atomic.h" | 10 #include "vm/atomic.h" |
| 11 #include "vm/code_patcher.h" | 11 #include "vm/code_patcher.h" |
| 12 #include "vm/instructions.h" |
| 12 #include "vm/isolate.h" | 13 #include "vm/isolate.h" |
| 13 #include "vm/json_stream.h" | 14 #include "vm/json_stream.h" |
| 14 #include "vm/lockers.h" | 15 #include "vm/lockers.h" |
| 15 #include "vm/native_symbol.h" | 16 #include "vm/native_symbol.h" |
| 16 #include "vm/object.h" | 17 #include "vm/object.h" |
| 17 #include "vm/os.h" | 18 #include "vm/os.h" |
| 18 #include "vm/profiler.h" | 19 #include "vm/profiler.h" |
| 19 #include "vm/reusable_handles.h" | 20 #include "vm/reusable_handles.h" |
| 20 #include "vm/signal_handler.h" | 21 #include "vm/signal_handler.h" |
| 21 #include "vm/simulator.h" | 22 #include "vm/simulator.h" |
| 22 #include "vm/stack_frame.h" | 23 #include "vm/stack_frame.h" |
| 23 | 24 |
| 24 namespace dart { | 25 namespace dart { |
| 25 | 26 |
| 26 | 27 |
| 27 #if defined(TARGET_OS_ANDROID) || defined(HOST_ARCH_ARM64) | 28 #if defined(TARGET_OS_ANDROID) || defined(HOST_ARCH_ARM64) |
| 28 DEFINE_FLAG(bool, profile, false, "Enable Sampling Profiler"); | 29 DEFINE_FLAG(bool, profile, false, "Enable Sampling Profiler"); |
| 29 #else | 30 #else |
| 30 DEFINE_FLAG(bool, profile, true, "Enable Sampling Profiler"); | 31 DEFINE_FLAG(bool, profile, true, "Enable Sampling Profiler"); |
| 31 #endif | 32 #endif |
| 32 DEFINE_FLAG(bool, trace_profiled_isolates, false, "Trace profiled isolates."); | 33 DEFINE_FLAG(bool, trace_profiled_isolates, false, "Trace profiled isolates."); |
| 33 DEFINE_FLAG(bool, trace_profiler, false, "Trace profiler."); | |
| 34 DEFINE_FLAG(int, profile_period, 1000, | 34 DEFINE_FLAG(int, profile_period, 1000, |
| 35 "Time between profiler samples in microseconds. Minimum 50."); | 35 "Time between profiler samples in microseconds. Minimum 50."); |
| 36 DEFINE_FLAG(int, profile_depth, 8, | 36 DEFINE_FLAG(int, profile_depth, 8, |
| 37 "Maximum number stack frames walked. Minimum 1. Maximum 255."); | 37 "Maximum number stack frames walked. Minimum 1. Maximum 255."); |
| 38 #if defined(PROFILE_NATIVE_CODE) || defined(USING_SIMULATOR) | 38 #if defined(PROFILE_NATIVE_CODE) || defined(USING_SIMULATOR) |
| 39 DEFINE_FLAG(bool, profile_vm, true, | 39 DEFINE_FLAG(bool, profile_vm, true, |
| 40 "Always collect native stack traces."); | 40 "Always collect native stack traces."); |
| 41 #else | 41 #else |
| 42 DEFINE_FLAG(bool, profile_vm, false, | 42 DEFINE_FLAG(bool, profile_vm, false, |
| 43 "Always collect native stack traces."); | 43 "Always collect native stack traces."); |
| (...skipping 202 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 246 | 246 |
| 247 Sample* SampleBuffer::ReserveSample() { | 247 Sample* SampleBuffer::ReserveSample() { |
| 248 ASSERT(samples_ != NULL); | 248 ASSERT(samples_ != NULL); |
| 249 uintptr_t cursor = AtomicOperations::FetchAndIncrement(&cursor_); | 249 uintptr_t cursor = AtomicOperations::FetchAndIncrement(&cursor_); |
| 250 // Map back into sample buffer range. | 250 // Map back into sample buffer range. |
| 251 cursor = cursor % capacity_; | 251 cursor = cursor % capacity_; |
| 252 return At(cursor); | 252 return At(cursor); |
| 253 } | 253 } |
| 254 | 254 |
| 255 | 255 |
| 256 static void SetPCMarkerIfSafe(Sample* sample) { | 256 // Used to locate the stack slot in Dart code that contains the return address. |
| 257 ASSERT(sample != NULL); | 257 // NOTE: Only handles the prologue. |
| 258 | 258 // NOTE: Architecture specific implementations below. |
| 259 uword* fp = reinterpret_cast<uword*>(sample->fp()); | 259 class ReturnAddressLocator : public ValueObject { |
| 260 uword* sp = reinterpret_cast<uword*>(sample->sp()); | 260 public: |
| 261 | 261 ReturnAddressLocator(uword pc, const Code& code) |
| 262 // If FP == SP, the pc marker hasn't been pushed. | 262 : pc_(pc), |
| 263 if (fp > sp) { | 263 code_(Code::ZoneHandle(code.raw())), |
| 264 #if defined(TARGET_OS_WINDOWS) | 264 is_optimized_(code.is_optimized()) { |
| 265 // If the fp is at the beginning of a page, it may be unsafe to access | 265 ASSERT(!code_.IsNull()); |
| 266 // the pc marker, because we are reading it from a different thread on | 266 ASSERT(code_.ContainsInstructionAt(pc_)); |
| 267 // Windows. The marker is below fp and the previous page may be a guard | 267 } |
| 268 // page. | 268 |
| 269 const intptr_t kPageMask = VirtualMemory::PageSize() - 1; | 269 bool is_code_optimized() { |
| 270 if ((sample->fp() & kPageMask) == 0) { | 270 return is_optimized_; |
| 271 } |
| 272 |
| 273 // Returns -1 on failure. |
| 274 intptr_t ReturnAddressRelativeToSP(); |
| 275 |
| 276 // Returns offset into code object. |
| 277 uword RelativePC() { |
| 278 return pc_ - code_.EntryPoint(); |
| 279 } |
| 280 |
| 281 uint8_t* CodePointer(uword offset) { |
| 282 const uword size = code_.Size(); |
| 283 ASSERT(offset < size); |
| 284 uint8_t* code_pointer = reinterpret_cast<uint8_t*>(code_.EntryPoint()); |
| 285 code_pointer += offset; |
| 286 return code_pointer; |
| 287 } |
| 288 |
| 289 private: |
| 290 const uword pc_; |
| 291 const Code& code_; |
| 292 const bool is_optimized_; |
| 293 }; |
| 294 |
| 295 |
| 296 #if defined(TARGET_ARCH_IA32) |
| 297 intptr_t ReturnAddressLocator::ReturnAddressRelativeToSP() { |
| 298 const uword offset = RelativePC(); |
| 299 const uword size = code_.Size(); |
| 300 if (is_optimized_) { |
| 301 // 0: push ebp |
| 302 // 1: mov ebp, esp |
| 303 // 3: ... |
| 304 if (offset == 0x0) { |
| 305 // Stack layout: |
| 306 // 0 RETURN ADDRESS. |
| 307 return 0; |
| 308 } |
| 309 if (offset == 0x1) { |
| 310 // Stack layout: |
| 311 // 0 CALLER FRAME POINTER |
| 312 // 1 RETURN ADDRESS |
| 313 return 1; |
| 314 } |
| 315 return -1; |
| 316 } else { |
| 317 // 0x00: mov edi, function |
| 318 // 0x05: incl (inc usage count) <-- this is optional. |
| 319 // 0x08: cmpl (compare usage count) |
| 320 // 0x0f: jump to optimize function |
| 321 // 0x15: push ebp |
| 322 // 0x16: mov ebp, esp |
| 323 // 0x18: ... |
| 324 ASSERT(size >= 0x08); |
| 325 const uword incl_offset = 0x05; |
| 326 const uword incl_length = 0x03; |
| 327 const uint8_t incl_op_code = 0xFF; |
| 328 const bool has_incl = (*CodePointer(incl_offset) == incl_op_code); |
| 329 const uword push_fp_offset = has_incl ? 0x15 : 0x15 - incl_length; |
| 330 if (offset <= push_fp_offset) { |
| 331 // Stack layout: |
| 332 // 0 RETURN ADDRESS. |
| 333 return 0; |
| 334 } |
| 335 if (offset == (push_fp_offset + 1)) { |
| 336 // Stack layout: |
| 337 // 0 CALLER FRAME POINTER |
| 338 // 1 RETURN ADDRESS |
| 339 return 1; |
| 340 } |
| 341 return -1; |
| 342 } |
| 343 UNREACHABLE(); |
| 344 return -1; |
| 345 } |
| 346 #elif defined(TARGET_ARCH_X64) |
| 347 intptr_t ReturnAddressLocator::ReturnAddressRelativeToSP() { |
| 348 const uword offset = RelativePC(); |
| 349 const uword size = code_.Size(); |
| 350 if (is_optimized_) { |
| 351 // 0x00: leaq (load pc marker) |
| 352 // 0x07: movq (load pool pointer) |
| 353 // 0x0c: push rpb |
| 354 // 0x0d: movq rbp, rsp |
| 355 // 0x10: ... |
| 356 const uword push_fp_offset = 0x0c; |
| 357 if (offset <= push_fp_offset) { |
| 358 // Stack layout: |
| 359 // 0 RETURN ADDRESS. |
| 360 return 0; |
| 361 } |
| 362 if (offset == (push_fp_offset + 1)) { |
| 363 // Stack layout: |
| 364 // 0 CALLER FRAME POINTER |
| 365 // 1 RETURN ADDRESS |
| 366 return 1; |
| 367 } |
| 368 return -1; |
| 369 } else { |
| 370 // 0x00: leaq (load pc marker) |
| 371 // 0x07: movq (load pool pointer) |
| 372 // 0x0c: movq (load function) |
| 373 // 0x13: incl (inc usage count) <-- this is optional. |
| 374 // 0x16: cmpl (compare usage count) |
| 375 // 0x1d: jl + 0x |
| 376 // 0x23: jmp [pool pointer] |
| 377 // 0x27: push rbp |
| 378 // 0x28: movq rbp, rsp |
| 379 // 0x2b: ... |
| 380 ASSERT(size >= 0x16); |
| 381 const uword incl_offset = 0x13; |
| 382 const uword incl_length = 0x03; |
| 383 const uint8_t incl_op_code = 0xFF; |
| 384 const bool has_incl = (*CodePointer(incl_offset) == incl_op_code); |
| 385 const uword push_fp_offset = has_incl ? 0x27 : 0x27 - incl_length; |
| 386 if (offset <= push_fp_offset) { |
| 387 // Stack layout: |
| 388 // 0 RETURN ADDRESS. |
| 389 return 0; |
| 390 } |
| 391 if (offset == (push_fp_offset + 1)) { |
| 392 // Stack layout: |
| 393 // 0 CALLER FRAME POINTER |
| 394 // 1 RETURN ADDRESS |
| 395 return 1; |
| 396 } |
| 397 return -1; |
| 398 } |
| 399 UNREACHABLE(); |
| 400 return -1; |
| 401 } |
| 402 #elif defined(TARGET_ARCH_ARM) |
| 403 intptr_t ReturnAddressLocator::ReturnAddressRelativeToSP() { |
| 404 return -1; |
| 405 } |
| 406 #elif defined(TARGET_ARCH_ARM64) |
| 407 intptr_t ReturnAddressLocator::ReturnAddressRelativeToSP() { |
| 408 return -1; |
| 409 } |
| 410 #elif defined(TARGET_ARCH_MIPS) |
| 411 intptr_t ReturnAddressLocator::ReturnAddressRelativeToSP() { |
| 412 return -1; |
| 413 } |
| 414 #else |
| 415 #error ReturnAddressLocator implementation missing for this architecture. |
| 416 #endif |
| 417 |
| 418 |
| 419 FixTopFrameVisitor::FixTopFrameVisitor(Isolate* isolate) |
| 420 : SampleVisitor(isolate), |
| 421 vm_isolate_(Dart::vm_isolate()) { |
| 422 } |
| 423 |
| 424 |
| 425 void FixTopFrameVisitor::VisitSample(Sample* sample) { |
| 426 if (sample->processed()) { |
| 427 // Already processed. |
| 428 return; |
| 429 } |
| 430 REUSABLE_CODE_HANDLESCOPE(isolate()); |
| 431 // Mark that we've processed this sample. |
| 432 sample->set_processed(true); |
| 433 // Lookup code object for leaf frame. |
| 434 Code& code = reused_code_handle.Handle(); |
| 435 code = FindCodeForPC(sample->At(0)); |
| 436 sample->set_leaf_frame_is_dart(!code.IsNull()); |
| 437 if (!code.IsNull() && (code.compile_timestamp() > sample->timestamp())) { |
| 438 // Code compiled after sample. Ignore. |
| 439 return; |
| 440 } |
| 441 if (sample->leaf_frame_is_dart()) { |
| 442 CheckForMissingDartFrame(code, sample); |
| 443 } |
| 444 } |
| 445 |
| 446 |
| 447 void FixTopFrameVisitor::CheckForMissingDartFrame(const Code& code, |
| 448 Sample* sample) const { |
| 449 // Some stubs (and intrinsics) do not push a frame onto the stack leaving |
| 450 // the frame pointer in the caller. |
| 451 // |
| 452 // PC -> STUB |
| 453 // FP -> DART3 <-+ |
| 454 // DART2 <-| <- TOP FRAME RETURN ADDRESS. |
| 455 // DART1 <-| |
| 456 // ..... |
| 457 // |
| 458 // In this case, traversing the linked stack frames will not collect a PC |
| 459 // inside DART3. The stack will incorrectly be: STUB, DART2, DART1. |
| 460 // In Dart code, after pushing the FP onto the stack, an IP in the current |
| 461 // function is pushed onto the stack as well. This stack slot is called |
| 462 // the PC marker. We can use the PC marker to insert DART3 into the stack |
| 463 // so that it will correctly be: STUB, DART3, DART2, DART1. Note the |
| 464 // inserted PC may not accurately reflect the true return address into DART3. |
| 465 ASSERT(!code.IsNull()); |
| 466 |
| 467 // The pc marker is our current best guess of a return address. |
| 468 uword return_address = sample->pc_marker(); |
| 469 |
| 470 // Attempt to find a better return address. |
| 471 ReturnAddressLocator ral(sample->At(0), code); |
| 472 ReturnPattern rp(sample->At(0)); |
| 473 |
| 474 intptr_t return_address_slot = ral.ReturnAddressRelativeToSP(); |
| 475 if (return_address_slot != -1) { |
| 476 // In prologue, located the return address. |
| 477 ASSERT(return_address_slot >= 0); |
| 478 ASSERT(return_address_slot < Sample::kStackBufferSizeInWords); |
| 479 return_address = sample->GetStackBuffer()[return_address_slot]; |
| 480 } else if (rp.IsValid()) { |
| 481 // In epilogue, located the return address. |
| 482 return_address = sample->possible_return_address(); |
| 483 } else { |
| 484 // Could not find a better return address than the pc_marker. |
| 485 if (code.ContainsInstructionAt(return_address)) { |
| 486 // PC marker is in the same code as pc, no missing frame. |
| 271 return; | 487 return; |
| 272 } | 488 } |
| 273 #endif | 489 } |
| 274 uword* pc_marker_ptr = fp + kPcMarkerSlotFromFp; | 490 |
| 275 // MSan/ASan are unaware of frames initialized by generated code. | 491 if (return_address != 0) { |
| 276 MSAN_UNPOISON(pc_marker_ptr, kWordSize); | 492 sample->InsertCallerForTopFrame(return_address); |
| 277 ASAN_UNPOISON(pc_marker_ptr, kWordSize); | 493 } |
| 278 sample->set_pc_marker(*pc_marker_ptr); | 494 } |
| 279 } | 495 |
| 496 |
| 497 bool FixTopFrameVisitor::ContainedInDartCodeHeaps(uword pc) const { |
| 498 return isolate()->heap()->CodeContains(pc) || |
| 499 vm_isolate()->heap()->CodeContains(pc); |
| 500 } |
| 501 |
| 502 |
| 503 RawCode* FixTopFrameVisitor::FindCodeForPC(uword pc) const { |
| 504 // Check current isolate for pc. |
| 505 if (isolate()->heap()->CodeContains(pc)) { |
| 506 return Code::LookupCode(pc); |
| 507 } |
| 508 // Check VM isolate for pc. |
| 509 if (vm_isolate()->heap()->CodeContains(pc)) { |
| 510 return Code::LookupCodeInVmIsolate(pc); |
| 511 } |
| 512 return Code::null(); |
| 280 } | 513 } |
| 281 | 514 |
| 282 | 515 |
| 283 // Given an exit frame, walk the Dart stack. | 516 // Given an exit frame, walk the Dart stack. |
| 284 class ProfilerDartExitStackWalker : public ValueObject { | 517 class ProfilerDartExitStackWalker : public ValueObject { |
| 285 public: | 518 public: |
| 286 ProfilerDartExitStackWalker(Isolate* isolate, Sample* sample) | 519 ProfilerDartExitStackWalker(Isolate* isolate, Sample* sample) |
| 287 : sample_(sample), | 520 : sample_(sample), |
| 288 frame_iterator_(isolate) { | 521 frame_iterator_(isolate) { |
| 289 ASSERT(sample_ != NULL); | 522 ASSERT(sample_ != NULL); |
| (...skipping 278 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 568 | 801 |
| 569 Sample* sample_; | 802 Sample* sample_; |
| 570 const uword stack_upper_; | 803 const uword stack_upper_; |
| 571 const uword original_pc_; | 804 const uword original_pc_; |
| 572 const uword original_fp_; | 805 const uword original_fp_; |
| 573 const uword original_sp_; | 806 const uword original_sp_; |
| 574 uword lower_bound_; | 807 uword lower_bound_; |
| 575 }; | 808 }; |
| 576 | 809 |
| 577 | 810 |
| 811 static void SetPCMarkerIfSafe(Sample* sample) { |
| 812 ASSERT(sample != NULL); |
| 813 |
| 814 uword* fp = reinterpret_cast<uword*>(sample->fp()); |
| 815 uword* sp = reinterpret_cast<uword*>(sample->sp()); |
| 816 |
| 817 // If FP == SP, the pc marker hasn't been pushed. |
| 818 if (fp > sp) { |
| 819 #if defined(TARGET_OS_WINDOWS) |
| 820 // If the fp is at the beginning of a page, it may be unsafe to access |
| 821 // the pc marker, because we are reading it from a different thread on |
| 822 // Windows. The marker is below fp and the previous page may be a guard |
| 823 // page. |
| 824 const intptr_t kPageMask = VirtualMemory::PageSize() - 1; |
| 825 if ((sample->fp() & kPageMask) == 0) { |
| 826 return; |
| 827 } |
| 828 #endif |
| 829 uword* pc_marker_ptr = fp + kPcMarkerSlotFromFp; |
| 830 // MSan/ASan are unaware of frames initialized by generated code. |
| 831 MSAN_UNPOISON(pc_marker_ptr, kWordSize); |
| 832 ASAN_UNPOISON(pc_marker_ptr, kWordSize); |
| 833 sample->set_pc_marker(*pc_marker_ptr); |
| 834 } |
| 835 } |
| 836 |
| 837 |
| 838 static void GrabPossibleReturnValue(Sample* sample) { |
| 839 ASSERT(sample != NULL); |
| 840 // On other architectures possible_return_address is filled in from a |
| 841 // register that was captured by the signal handler. |
| 842 #if defined(TARGET_ARCH_X64) || defined(TARGET_ARCH_IA32) || \ |
| 843 defined(USING_SIMULATOR) |
| 844 uword* sp = reinterpret_cast<uword*>(sample->sp()); |
| 845 if (sp != NULL) { |
| 846 sample->set_possible_return_address(*sp); |
| 847 } |
| 848 #endif |
| 849 } |
| 850 |
| 851 |
| 852 static void FillStackBuffer(Sample* sample) { |
| 853 ASSERT(sample != NULL); |
| 854 uword* sp = reinterpret_cast<uword*>(sample->sp()); |
| 855 uword* stack_buffer = sample->GetStackBuffer(); |
| 856 if (sp != NULL) { |
| 857 for (intptr_t i = 0; i < Sample::kStackBufferSizeInWords; i++) { |
| 858 stack_buffer[i] = *sp; |
| 859 sp++; |
| 860 } |
| 861 } |
| 862 } |
| 863 |
| 864 |
| 578 void Profiler::RecordSampleInterruptCallback( | 865 void Profiler::RecordSampleInterruptCallback( |
| 579 const InterruptedThreadState& state, | 866 const InterruptedThreadState& state, |
| 580 void* data) { | 867 void* data) { |
| 581 Isolate* isolate = reinterpret_cast<Isolate*>(data); | 868 Isolate* isolate = reinterpret_cast<Isolate*>(data); |
| 582 if ((isolate == NULL) || (Dart::vm_isolate() == NULL)) { | 869 if ((isolate == NULL) || (Dart::vm_isolate() == NULL)) { |
| 583 // No isolate. | 870 // No isolate. |
| 584 return; | 871 return; |
| 585 } | 872 } |
| 586 | 873 |
| 587 ASSERT(isolate != Dart::vm_isolate()); | 874 ASSERT(isolate != Dart::vm_isolate()); |
| (...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 672 } | 959 } |
| 673 #endif | 960 #endif |
| 674 // Increment counter for vm tag. | 961 // Increment counter for vm tag. |
| 675 VMTagCounters* counters = isolate->vm_tag_counters(); | 962 VMTagCounters* counters = isolate->vm_tag_counters(); |
| 676 ASSERT(counters != NULL); | 963 ASSERT(counters != NULL); |
| 677 counters->Increment(vm_tag); | 964 counters->Increment(vm_tag); |
| 678 sample->set_vm_tag(vm_tag); | 965 sample->set_vm_tag(vm_tag); |
| 679 sample->set_user_tag(isolate->user_tag()); | 966 sample->set_user_tag(isolate->user_tag()); |
| 680 sample->set_sp(sp); | 967 sample->set_sp(sp); |
| 681 sample->set_fp(state.fp); | 968 sample->set_fp(state.fp); |
| 969 sample->set_possible_return_address(state.ra); |
| 970 GrabPossibleReturnValue(sample); |
| 971 FillStackBuffer(sample); |
| 682 #if !(defined(TARGET_OS_WINDOWS) && defined(TARGET_ARCH_X64)) | 972 #if !(defined(TARGET_OS_WINDOWS) && defined(TARGET_ARCH_X64)) |
| 683 // It is never safe to read other thread's stack unless on Win64 | 973 // It is never safe to read other thread's stack unless on Win64 |
| 684 // other thread is inside Dart code. | 974 // other thread is inside Dart code. |
| 685 SetPCMarkerIfSafe(sample); | 975 SetPCMarkerIfSafe(sample); |
| 686 #endif | 976 #endif |
| 687 | 977 |
| 688 // Walk the call stack. | 978 // Walk the call stack. |
| 689 if (FLAG_profile_vm) { | 979 if (FLAG_profile_vm) { |
| 690 // Always walk the native stack collecting both native and Dart frames. | 980 // Always walk the native stack collecting both native and Dart frames. |
| 691 ProfilerNativeStackWalker stackWalker(sample, | 981 ProfilerNativeStackWalker stackWalker(sample, |
| (...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 729 state.pc, | 1019 state.pc, |
| 730 state.fp, | 1020 state.fp, |
| 731 sp); | 1021 sp); |
| 732 stackWalker.walk(); | 1022 stackWalker.walk(); |
| 733 #endif | 1023 #endif |
| 734 } | 1024 } |
| 735 } | 1025 } |
| 736 } | 1026 } |
| 737 | 1027 |
| 738 } // namespace dart | 1028 } // namespace dart |
| OLD | NEW |